Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

module: fix decorator multiple-use inconsistency #1632

Merged
merged 3 commits into from
Sep 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions sopel/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,10 @@ def remove_plugin(self, plugin, callables, jobs, shutdowns, urls):
# remove URL callback handlers
if self.memory.contains('url_callbacks'):
for func in urls:
regex = func.url_regex
if func == self.memory['url_callbacks'].get(regex):
self.unregister_url_callback(regex)
regexes = func.url_regex
for regex in regexes:
if func == self.memory['url_callbacks'].get(regex):
self.unregister_url_callback(regex)
except: # noqa
# TODO: consider logging?
raise # re-raised
Expand Down Expand Up @@ -340,7 +341,8 @@ def register(self, callables, jobs, shutdowns, urls):
self.scheduler.add_job(job)

for func in urls:
self.register_url_callback(func.url_regex, func)
for regex in func.url_regex:
self.register_url_callback(regex, func)

def part(self, channel, msg=None):
"""Part a channel."""
Expand Down
90 changes: 61 additions & 29 deletions sopel/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

from __future__ import unicode_literals, absolute_import, print_function, division

import re
import functools
import re

NOLIMIT = 1
"""Return value for ``callable``\\s, which suppresses rate limiting for the call.
Expand Down Expand Up @@ -61,7 +61,7 @@ def unblockable(function):
return function


def interval(*args):
def interval(*intervals):
"""Decorates a function to be called by the bot every X seconds.

This decorator can be used multiple times for multiple intervals, or all
Expand All @@ -76,10 +76,11 @@ def interval(*args):
There is no guarantee that the bot is connected to a server or joined a
channel when the function is called, so care must be taken.

Example:::
Example::

from sopel import module

import sopel.module
@sopel.module.interval(5)
@module.interval(5)
def spam_every_5s(bot):
if "#here" in bot.channels:
bot.say("It has been five seconds!", "#here")
Expand All @@ -88,20 +89,20 @@ def spam_every_5s(bot):
def add_attribute(function):
if not hasattr(function, "interval"):
function.interval = []
for arg in args:
function.interval.append(arg)
for arg in intervals:
if arg not in function.interval:
function.interval.append(arg)
return function

return add_attribute


def rule(value):
def rule(*patterns):
"""Decorate a function to be called when a line matches the given pattern

This decorator can be used multiple times to add more rules.
Each argument is a regular expression which will trigger the function.

Args:
value: A regular expression which will trigger the function.
This decorator can be used multiple times to add more rules.

If the Sopel instance is in a channel, or sent a PRIVMSG, where a string
matching this expression is said, the function will execute. Note that
Expand All @@ -110,11 +111,20 @@ def rule(value):
Inside the regular expression, some special directives can be used. $nick
will be replaced with the nick of the bot and , or :, and $nickname will be
replaced with the nick of the bot.

.. versionchanged:: 7.0

The :func:`rule` decorator can be called with multiple positional
arguments, each used to add a rule. This is equivalent to decorating
the same function multiple times with this decorator.

"""
def add_attribute(function):
if not hasattr(function, "rule"):
function.rule = []
function.rule.append(value)
for value in patterns:
if value not in function.rule:
function.rule.append(value)
return function

return add_attribute
Expand All @@ -123,19 +133,20 @@ def add_attribute(function):
def thread(value):
"""Decorate a function to specify if it should be run in a separate thread.

:param bool value: if true, the function is called in a separate thread;
otherwise from the bot's main thread

Functions run in a separate thread (as is the default) will not prevent the
bot from executing other functions at the same time. Functions not run in a
separate thread may be started while other functions are still running, but
additional functions will not start until it is completed.

Args:
value: Either True or False. If True the function is called in
a separate thread. If False from the main thread.

"""
threaded = bool(value)

def add_attribute(function):
function.thread = value
function.thread = threaded
return function

return add_attribute


Expand Down Expand Up @@ -183,7 +194,9 @@ def commands(*command_list):
def add_attribute(function):
if not hasattr(function, "commands"):
function.commands = []
function.commands.extend(command_list)
for command in command_list:
if command not in function.commands:
function.commands.append(command)
return function
return add_attribute

Expand Down Expand Up @@ -215,7 +228,11 @@ def nickname_commands(*command_list):

"""
def add_attribute(function):
function.nickname_commands = [cmd for cmd in command_list]
if not hasattr(function, 'nickname_commands'):
function.nickname_commands = []
for cmd in command_list:
if cmd not in function.nickname_commands:
function.nickname_commands.append(cmd)
return function
return add_attribute

Expand Down Expand Up @@ -252,7 +269,9 @@ def event(*event_list):
def add_attribute(function):
if not hasattr(function, "event"):
function.event = []
function.event.extend(event_list)
for name in event_list:
if name not in function.event:
function.event.append(name)
return function
return add_attribute

Expand All @@ -265,7 +284,9 @@ def intent(*intent_list):
def add_attribute(function):
if not hasattr(function, "intents"):
function.intents = []
function.intents.extend(intent_list)
for name in intent_list:
if name not in function.intents:
function.intents.append(name)
return function
return add_attribute

Expand Down Expand Up @@ -474,7 +495,7 @@ def guarded(bot, trigger, *args, **kwargs):
return actual_decorator


def url(url_rule):
def url(*url_rules):
"""Decorate a function to handle URLs.

:param str url_rule: regex pattern to match URLs
Expand All @@ -487,6 +508,7 @@ def url(url_rule):
from sopel import module

@module.url(r'https://example.com/bugs/([a-z0-9]+)')
@module.url(r'https://short.com/([a-z0-9]+)')
def handle_example_bugs(bot, trigger, match):
bot.reply('Found bug ID #%s' % match.group(1))

Expand All @@ -496,6 +518,15 @@ def handle_example_bugs(bot, trigger, match):
Under the hood, when Sopel collects the decorated handler it uses
:meth:`sopel.bot.Sopel.register_url_callback` to register the handler.

.. versionchanged:: 7.0

The same function can be decorated multiple times with :func:`url`
to register different URL patterns.

.. versionchanged:: 7.0

More than one pattern can be provided as positional argument at once.

.. seealso::

To detect URLs, Sopel uses a matching pattern built from a list of URL
Expand All @@ -504,12 +535,13 @@ def handle_example_bugs(bot, trigger, match):

"""
def actual_decorator(function):
@functools.wraps(function)
def helper(bot, trigger, match=None):
match = match or trigger
return function(bot, trigger, match)
helper.url_regex = re.compile(url_rule)
return helper
if not hasattr(function, 'url_regex'):
function.url_regex = []
for url_rule in url_rules:
url_regex = re.compile(url_rule)
if url_regex not in function.url_regex:
function.url_regex.append(url_regex)
return function
return actual_decorator


Expand Down
Loading