Skip to content

Commit

Permalink
Added named arguments to the match decorator.
Browse files Browse the repository at this point in the history
Author: Keegan Carruthers-Smith
Merge Request: http://code.launchpad.net/~keegan-csmith/ibid/named-groups-656201/+merge/51159
Approved by: Stefano Rivera, Max Rabkin
Fixes LP: #656201
  • Loading branch information
keegancsmith authored and Tarmac committed Mar 23, 2011
2 parents fa89af1 + 9a864e9 commit 0660522
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 25 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Expand Up @@ -24,3 +24,4 @@ Ibid is also the recipient of contributions from the following list of authors:
* Kevin Woodland
* Guy Halse
* Dominic Cleal
* Keegan Carruthers-Smith
17 changes: 16 additions & 1 deletion docs/api/ibid.plugins.rst
Expand Up @@ -147,7 +147,7 @@ Decorators
Selector expands to
============ ==========================================
``{alpha}`` ``[a-zA-Z]+``
``{any}`` ``.*``
``{any}`` ``.+``
``{chunk}`` ``\S+``
``{digits}`` ``\d+``
``{number}`` ``\d*\.?\d+``
Expand All @@ -169,6 +169,21 @@ Decorators

@match(r'^(?:foo|bar)\s+(\S+)$', simple=False)

Optionally you can specify keyword arguments, which will get passed on to
the function. The syntax is ``{keyword:selector}``, where the keywords will
get passed onto the decorated method::

@match(r'I am {nick:chunk} on {network:chunk}')
def register(self, event, network, nick):
pass

The above match is equivalent to this non-simple version::

@match(r'^I\s+am\s+(?P<nick>\S+)\s+on\s+(?P<network>\S+)$', simple=False)

Note that you cannot mix positional and keyword arguments. All your
selectors must either be named, or all be unnamed.

*version* can be set to one of:

``'clean'``
Expand Down
47 changes: 37 additions & 10 deletions ibid/plugins/__init__.py
@@ -1,4 +1,4 @@
# Copyright (c) 2008-2010, Michael Gorven, Stefano Rivera
# Copyright (c) 2008-2011, Michael Gorven, Stefano Rivera, Keegan Carruthers-Smith
# Released under terms of the MIT/X/Expat Licence. See COPYING for details.

from copy import copy
Expand All @@ -23,7 +23,7 @@ def pluginPackagePaths(name):
if not os.path.exists(os.path.join(x, *package + ['__init__.py']))]

import ibid
from ibid.compat import json
from ibid.compat import json, defaultdict
from ibid.utils import url_regex

__path__ = pluginPackagePaths(__name__) + __path__
Expand Down Expand Up @@ -142,10 +142,27 @@ def process(self, event):
event.message[method.message_version])
if match is not None:
args = match.groups()
kwargs = match.groupdict()
if kwargs:
assert len(args) == len(kwargs), (
"Can't intermix named and positional arguments.")
# Convert the names from the %s__%d_ format to %s
args = {}
for name, value in kwargs.iteritems():
name = re.match(r'^(\S+?)(?:__\d+_)?$', name).group(1)
if args.get(name, None) is None:
args[name] = value
else:
assert value is None, (
'named argument %s was matched more '
'than once.' % name)
if args is not None:
if (not getattr(method, 'auth_required', False)
or auth_responses(event, self.permission)):
method(event, *args)
if isinstance(args, dict):
method(event, **args)
else:
method(event, *args)
elif not getattr(method, 'auth_fallthrough', True):
event.processed = True

Expand Down Expand Up @@ -214,7 +231,7 @@ def handler(function):
def _match_sub_selectors(regex):
selector_patterns = {
'alpha' : r'[a-zA-Z]+',
'any' : r'.*',
'any' : r'.+',
'chunk' : r'\S+',
'digits' : r'\d+',
'number' : r'\d*\.?\d+',
Expand All @@ -224,12 +241,22 @@ def _match_sub_selectors(regex):

regex = regex.replace(' ', r'(?:\s+)')

for pattern in re.finditer('{(%s)}' % '|'.join(selector_patterns.keys()),
regex):
pattern = pattern.group(1)
old = '{%s}' % pattern
new = '(%s)' % selector_patterns[pattern]
regex = regex.replace(old, new)
name_count = defaultdict(int)
def selector_to_re(match):
name = match.group(1)
pattern = match.group(2)

if name is None:
return '(%s)' % selector_patterns[pattern]

# Prevent conflicts when reusing a name
name_count[name] += 1
name = '%s__%d_' % (name, name_count[name])

return '(?P<%s>%s)' % (name, selector_patterns[pattern])

regex = re.sub(r'{(?:(\w+):)?(%s)}' % '|'.join(selector_patterns.keys()),
selector_to_re, regex)

if not regex.startswith('^'):
regex = '^' + regex
Expand Down
8 changes: 6 additions & 2 deletions ibid/plugins/factoid.py
Expand Up @@ -684,9 +684,12 @@ def append(self, event, name, number, pattern, is_regex, suffix):
event.identity, event.sender['connection'])
event.addresponse(True)

@match(r'^(.+?)(?:\s+#(\d+)|\s+/(.+?)/(r?))?\s*(?:~=|=~)\s*([sy](?P<sep>.).+(?P=sep).*(?P=sep)[gir]*)$')
@match(r'(?P<name>.+?)'
r'(?: #{number:digits}| /(?P<pattern>.+?)/(?P<is_regex>r?))?'
r'\s*(?:~=|=~)\s*'
r'(?P<operation>[sy](?P<sep>.).+(?P=sep).*(?P=sep)[gir]*)$')
@authorise(fallthrough=False)
def modify(self, event, name, number, pattern, is_regex, operation, separator):
def modify(self, event, name, number, pattern, is_regex, operation, sep):
factoids = get_factoid(event.session, name, number, pattern, is_regex, all=True)
if len(factoids) == 0:
if pattern:
Expand All @@ -707,6 +710,7 @@ def modify(self, event, name, number, pattern, is_regex, operation, separator):
return

# Not very pythonistic, but escaping is a nightmare.
separator = sep
parts = [[]]
pos = 0
while pos < len(operation):
Expand Down
14 changes: 7 additions & 7 deletions ibid/plugins/memo.py
Expand Up @@ -151,18 +151,18 @@ def tell(self, event, how, who, memo):
'source': to.source,
})

@match(r'^(?:delete|forget)\s+(?:my\s+)?'
r'(?:(first|last|\d+(?:st|nd|rd|th)?)\s+)?' # 1st way to specify number
r'(?:memo|message|msg)\s+'
r'(?(1)|#?(\d+)\s+)?' # 2nd way
r'(?:for|to)\s+(.+?)(?:\s+on\s+(\S+))?$')
@match(r'(?:delete|forget) (?:my )?'
r'(?:(?P<num>first|last|\d+(?:st|nd|rd|th)?) )?' # 1st way to specify number
r'(?:memo|message|msg) '
r'(?(1)|#?{num:digits} )?' # 2nd way
r'(?:for|to) (?P<who>.+?)(?: on {source:chunk})?')
@authorise(fallthrough=False)
def forget(self, event, num1, num2, who, source):
def forget(self, event, num, who, source):
if not source:
source = event.source
else:
source = source.lower()
number = num1 or num2 or 'last'
number = num or 'last'
number = number.lower()
if number == 0:
# Don't wrap around to last message, that'd be unexpected
Expand Down
6 changes: 3 additions & 3 deletions ibid/plugins/network.py
Expand Up @@ -528,10 +528,10 @@ def _load_services(self):
break
self.protocols[proto.lower()].append(port)

@match(r'^(?:(.+)\s+)?ports?(?:\s+numbers?)?(?(1)|\s+for\s+(.+))$')
def portfor(self, event, proto1, proto2):
@match(r'(?:{proto:any} )?ports?(?: numbers?)?(?(1)| for {proto:any})')
def portfor(self, event, proto):
self._load_services()
protocol = (proto1 or proto2).lower()
protocol = proto.lower()
if protocol in self.protocols:
event.addresponse(human_join(self.protocols[protocol]))
else:
Expand Down
4 changes: 2 additions & 2 deletions ibid/plugins/social.py
Expand Up @@ -27,7 +27,7 @@ class LastFm(Processor):

features = ('lastfm',)

@match(r'^last\.?fm\s+for\s+(\S+?)\s*$')
@match(r'last\.?fm for {username:chunk}')
def listsongs(self, event, username):
songs = feedparser.parse('http://ws.audioscrobbler.com/1.0/user/%s/recenttracks.rss?%s' % (username, time()))
if songs['bozo']:
Expand Down Expand Up @@ -147,7 +147,7 @@ def latest(self, event, service_name, user):
def twitter(self, event, id):
self.update(event, u'twitter', id)

@match(r'^https?://(?:www\.)?identi.ca/notice/(\d+)$')
@match(r'https?://(?:www\.)?identi.ca/notice/{id:digits}')
def identica(self, event, id):
self.update(event, u'identica', id)

Expand Down

0 comments on commit 0660522

Please sign in to comment.