Skip to content

Commit

Permalink
bot,rules,loader: delegate URL callbacks management to the rule system
Browse files Browse the repository at this point in the history
  • Loading branch information
Exirel committed Jul 17, 2020
1 parent 55a60f7 commit 9c19561
Show file tree
Hide file tree
Showing 10 changed files with 848 additions and 53 deletions.
32 changes: 15 additions & 17 deletions sopel/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,6 @@ def remove_plugin(self, plugin, callables, jobs, shutdowns, urls):
self._rules_manager.unregister_plugin(name)
self._scheduler.unregister_plugin(name)
self.unregister_shutdowns(shutdowns)
self.unregister_urls(urls)

# remove plugin from registry
del self._plugins[name]
Expand Down Expand Up @@ -573,22 +572,21 @@ def unregister_shutdowns(self, shutdowns):

def register_urls(self, urls):
for func in urls:
for regex in func.url_regex:
self.register_url_callback(regex, func)
callable_name = getattr(func, "__name__", 'UNKNOWN')
LOGGER.debug(
'URL Callback added "%s" for URL pattern "%s"',
callable_name,
regex)

def unregister_urls(self, urls):
if "url_callbacks" in self.memory:
for func in urls:
regexes = func.url_regex
for regex in regexes:
if func == self.memory['url_callbacks'].get(regex):
self.unregister_url_callback(regex, func)
LOGGER.debug('URL Callback unregistered: %r', regex)
url_regex = getattr(func, 'url_regex', [])
url_lazy_loader = getattr(func, 'url_lazy_loader', None)

if url_regex:
rule = plugin_rules.URLCallback.from_callable(
self.settings, func)
self._rules_manager.register_url_callback(rule)

if url_lazy_loader:
try:
rule = plugin_rules.URLCallback.from_callable_lazy(
self.settings, func)
self._rules_manager.register_url_callback(rule)
except plugins.exceptions.PluginError as err:
LOGGER.error('Can not register URL callback: %s', err)

@deprecated(
reason="Replaced by `say` method.",
Expand Down
40 changes: 36 additions & 4 deletions sopel/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ def clean_callable(func, config):
func.global_rate = getattr(func, 'global_rate', 0)
func.unblockable = getattr(func, 'unblockable', False)

if not is_triggerable(func):
# Adding the remaining default attributes below is potentially confusing
# to other code (and a waste of memory) for non-triggerable functions.
if not is_triggerable(func) and not is_url_callback(func):
# Adding the remaining default attributes below is potentially
# confusing to other code (and a waste of memory) for jobs.
return

func.echo = getattr(func, 'echo', False)
Expand Down Expand Up @@ -152,6 +152,7 @@ def is_limitable(obj):
'nickname_commands',
'action_commands',
'url_regex',
'url_lazy_loader',
)
allowed = any(hasattr(obj, attr) for attr in allowed_attrs)

Expand All @@ -173,10 +174,12 @@ def is_triggerable(obj):
Many of the decorators defined in :mod:`sopel.plugin` make the
decorated function a triggerable object.
"""
forbidden_attrs = (
'interval',
'url_regex',
'url_lazy_loader',
)
forbidden = any(hasattr(obj, attr) for attr in forbidden_attrs)

Expand All @@ -195,6 +198,35 @@ def is_triggerable(obj):
return allowed and not forbidden


def is_url_callback(obj):
"""Check if ``obj`` can handle a URL callback.
:param obj: any :term:`function` to check
:return: ``True`` if ``obj`` can handle a URL callback
A URL callback handler is a callable that will be used by the bot to
handle a particular URL in an IRC message.
.. seealso::
Both :func:`sopel.plugin.url` :func:`sopel.plugin.url_lazy` make the
decorated function a URL callback handler.
"""
forbidden_attrs = (
'interval',
)
forbidden = any(hasattr(obj, attr) for attr in forbidden_attrs)

allowed_attrs = (
'url_regex',
'url_lazy_loader',
)
allowed = any(hasattr(obj, attr) for attr in allowed_attrs)

return allowed and not forbidden


def clean_module(module, config):
callables = []
shutdowns = []
Expand All @@ -210,7 +242,7 @@ def clean_module(module, config):
elif hasattr(obj, 'interval'):
clean_callable(obj, config)
jobs.append(obj)
elif hasattr(obj, 'url_regex'):
elif is_url_callback(obj):
clean_callable(obj, config)
urls.append(obj)
return callables, jobs, shutdowns, urls
10 changes: 7 additions & 3 deletions sopel/modules/url.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,11 +277,11 @@ def check_callbacks(bot, url):
:param bot: Sopel instance
:param str url: URL to check
:return: True if ``url`` is excluded or matches any URL Callback pattern
:return: True if ``url`` is excluded or matches any URL callback pattern
This function looks at the ``bot.memory`` for ``url_exclude`` patterns and
it returns ``True`` if any matches the given ``url``. Otherwise, it looks
at the ``bot``'s URL Callback patterns, and it returns ``True`` if any
at the ``bot``'s URL callback patterns, and it returns ``True`` if any
matches, ``False`` otherwise.
.. seealso::
Expand All @@ -297,7 +297,11 @@ def check_callbacks(bot, url):
"""
# Check if it matches the exclusion list first
matched = any(regex.search(url) for regex in bot.memory['url_exclude'])
return matched or any(bot.search_url_callbacks(url))
return (
matched or
any(bot.search_url_callbacks(url)) or
bot.rules.check_url_callback(bot, url)
)


def find_title(url, verify=True):
Expand Down
66 changes: 62 additions & 4 deletions sopel/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
'thread',
'unblockable',
'url',
'url_lazy',
]


Expand Down Expand Up @@ -913,11 +914,14 @@ def url(*url_rules):
def handle_example_bugs(bot, trigger, match):
bot.reply('Found bug ID #%s' % match.group(1))
This should be used rather than the matching in ``trigger``, in order to
support e.g. the ``.title`` command.
Both ``trigger`` and ``match`` represent the same match on the URL. The
``match`` object is a Python built-in :func:`match object <re.match>`, while
``trigger`` is an usual :class:`~sopel.trigger.Trigger` object.
Under the hood, when Sopel collects the decorated handler it uses
:meth:`sopel.bot.Sopel.register_url_callback` to register the handler.
Under the hood, when Sopel collects the decorated handler it uses an
instance of :class:`sopel.plugins.rules.URLCallback` to register it to its
:attr:`rules manager <sopel.bot.Sopel.rules>` and its
:meth:`~sopel.plugins.rules.Manager.register_url_callback` method.
.. versionchanged:: 7.0
Expand All @@ -928,6 +932,12 @@ def handle_example_bugs(bot, trigger, match):
More than one pattern can be provided as positional argument at once.
.. versionchanged:: 7.1
The ``trigger`` parameter now represents the same match as the
``match`` parameter, making ``match`` an obsolete parameter, kept
for backward compatibility.
.. seealso::
To detect URLs, Sopel uses a matching pattern built from a list of URL
Expand All @@ -946,6 +956,54 @@ def actual_decorator(function):
return actual_decorator


def url_lazy(loader):
"""Decorate a function to handle URL, using lazy-loading for its regex.
:param loader: a callable to generate a list of regexes when the bot is
loading this URL callback
:type loader: :term:`function`
The ``loader`` function must accept a ``settings`` parameter and return a
list (or tuple) of compiled regular expressions::
import re
def loader(settings):
return [re.compile(r'<your_url_pattern>')]
It will be called by Sopel when the bot parses the plugin to register URL
callbacks to get its regexes. The ``settings`` argument will be the bot's
:class:`sopel.config.Config` object.
If the ``loader`` function raises an
:exc:`~sopel.plugins.exceptions.ImproperlyConfigured` exception, the URL
callback will be ignored. This exception can be used with a message that
will be logged as an error; it will not fail the plugin's loading.
The decorated function will behave like any other :func:`callable`::
from sopel import plugin
@plugin.url_lazy(loader)
def my_url_handler(bot, trigger):
bot.say('URL found: %s' % trigger.group(0))
.. versionadded:: 7.1
.. important::
There are few differences with the :func:`url` decorator:
* the decorated callable does **not** have a ``match`` parameter
* the decorated callable accepts **one and only one** ``loader``
"""
def decorator(function):
function.url_lazy_loader = loader
return function
return decorator


class example(object):
"""Decorate a function with an example, and optionally test output.
Expand Down
Loading

0 comments on commit 9c19561

Please sign in to comment.