"""
Matchers are the main user-facing API for `pyfpm`.
This module lets you unpack objects:
>>> unpacker = Unpacker()
>>> unpacker('head :: tail') << (1, 2, 3)
>>> unpacker.head
1
>>> unpacker.tail
(2, 3)
or function parameters:
>>> @match_args('[x:str, [y:int, z:int]]')
... def match(x, y, z):
... return (x, y, z)
>>> match('abc', (1, 2))
('abc', 1, 2)
You can also create simple matchers with lambda expressions:
>>> what_is_it = Matcher([
... ('_:int', lambda: 'an int'),
... ('_:str', lambda: 'a string'),
... ('x', lambda x: 'something else: %s' % x),
... ])
>>> what_is_it(10)
'an int'
>>> what_is_it('abc')
'a string'
>>> what_is_it({})
'something else: {}'
or more complex ones using a decorator:
>>> parse_options = Matcher()
>>> @parse_options.handler("['-h'|'--help', None]")
... def help():
... return 'help'
>>> @parse_options.handler("['-o'|'--optim', level:int] if 1<=level<=5")
... def set_optimization(level):
... return 'optimization level set to %d' % level
>>> @parse_options.handler("['-o'|'--optim', bad_level]")
... def bad_optimization(bad_level):
... return 'bad optimization level: %s' % bad_level
>>> @parse_options.handler('x')
... def unknown_options(x):
... return 'unknown options: %s' % repr(x)
>>> parse_options(('-h', None))
'help'
>>> parse_options(('--help', None))
'help'
>>> parse_options(('-o', 3))
'optimization level set to 3'
>>> parse_options(('-o', 0))
'bad optimization level: 0'
>>> parse_options(('-v', 'x'))
"unknown options: ('-v', 'x')"
"""
from functools import wraps
from pyfpm.parser import Parser, _get_caller_globals
from pyfpm.pattern import _basestring
[docs]class NoMatch(Exception):
"""
Thrown by matchers when no registered pattern could match the given object.
"""
[docs]class Matcher(object):
"""
Maps patterns to handler functions.
:param bindings: an optional list of pattern-handler pairs.
String patterns are automatically parsed.
:type bindings: iterable
:param context: an optional context for the :class:`Parser`.
If absent, it uses the caller's `globals()`
:type context: dict
"""
def __init__(self, bindings=[], context=None):
self.bindings = []
if context is None:
context = _get_caller_globals()
self.parser = Parser(context)
for pattern, handler in bindings:
self.register(pattern, handler)
[docs] def register(self, pattern, handler):
"""
Register a new pattern-handler pair. If the pattern is a string, it will
be parsed automatically.
:param pattern: Pattern or str -- the pattern
:param handler: callable -- the handler function for the pattern
"""
if isinstance(pattern, _basestring):
pattern = self.parser(pattern)
self.bindings.append((pattern, handler))
[docs] def match(self, obj, *args):
"""
Match the given object against the registerd patterns until the first
match. The corresponding handler gets called with `args` as
positional arguments and the match context as keyword arguments.
:param obj: the object to match the patterns with
:param args: the extra positional arguments that the handler function
will get called with
:raises: NoMatch -- if none of the patterns can match de object
Example:
>>> m = Matcher([
... ('head :: tail', lambda extra, head, tail: (extra, head, tail)),
... ('x', lambda extra, x: (extra, 'got something! %s' % x)),
... ])
>>> m.match('hello', 'yo!')
('yo!', 'got something! hello')
>>> m.match((1, 2, 3), 'numbers')
('numbers', 1, (2, 3))
"""
for pattern, handler in self.bindings:
match = pattern << obj
if match:
return handler(*args, **match.ctx)
raise NoMatch('no registered pattern could match %s' % repr(obj))
def __call__(self, obj, *args):
"""
Same as :func:`match`. Matcher instances can be called directly:
>>> m = Matcher([('_', lambda: 'yes')])
>>> m(0) == m.match(0)
True
"""
return self.match(obj, *args)
[docs] def handler(self, pattern):
"""
Decorator for registering handlers. It's an alternate syntax with the
same effect as :func:`register`:
>>> m = Matcher()
>>> @m.handler('x:int')
... def int_(x):
... return 'an int: %d' % x
>>> @m.handler('_')
... def any():
... return 'any'
>>> m(1)
'an int: 1'
>>> m(None)
'any'
"""
def _reg(function):
self.register(pattern, function)
return function
return _reg
def __eq__(self, other):
return (self.__class__ == other.__class__ and
self.bindings == other.bindings and
self.parser.context == other.parser.context)
def __repr__(self):
return '%s(%s)' % (self.__class__.__name__,
','.join('='.join((str(k), repr(v)))
for (k, v) in self.__dict__.items()))
[docs]def match_args(pattern, context=None):
"""
Decorator for matching a function's arglist.
:param pattern: Pattern or str -- the pattern
:param context: dict -- an optional context for the pattern parser. If
absent, it defaults to the caller's `globals()`.
Usage:
>>> @match_args('head::tail')
... def do_something(head, tail):
... return (head, tail)
>>> do_something(1, 2, 3, 4)
(1, (2, 3, 4))
"""
if isinstance(pattern, _basestring):
if context is None:
context = _get_caller_globals()
pattern = Parser(context)(pattern)
def wrapper(function):
@wraps(function)
def f(*args):
match = pattern.match(args)
if not match:
raise NoMatch("%s doesn't match %s" % (pattern, args))
return function(**match.ctx)
return f
return wrapper
class _UnpackerHelper(object):
def __init__(self, vars, pattern):
self.vars = vars
self.pattern = pattern
def _do(self, other):
match = self.pattern.match(other)
if not match:
raise NoMatch("%s doesn't match %s" % (self.pattern, other))
self.vars.update(match.ctx)
def __lshift__(self, other):
return self._do(other)
[docs]class Unpacker(object):
"""
Inline object unpacker. Usage:
>>> unpacker = Unpacker()
>>> unpacker('[x, [y, z]]') << (1, (2, 3))
>>> unpacker.x
1
>>> unpacker.y
2
>>> unpacker.z
3
"""
def __call__(self, pattern, context=None):
if isinstance(pattern, _basestring):
if context is None:
context = _get_caller_globals()
pattern = Parser(context)(pattern)
return _UnpackerHelper(self.__dict__, pattern)