"""
This module holds the actual pattern implementations.
End users should not normally have to deal with it, except for constructing
patterns programatically without making use of the pattern syntax parser.
"""
import re
try:
# python 2.x base string
_basestring = basestring
except NameError:
# python 3.x base string
_basestring = str
[docs]class Match(object):
"""
Represents the result of matching successfully a pattern against an
object. The `ctx` attribute is a :class:`dict` that contains the value for
each bound name in the pattern, if any.
"""
def __init__(self, ctx=None, value=None):
if ctx is None:
ctx = {}
self.ctx = ctx
self.value = value
def __eq__(self, other):
return (isinstance(other, Match) and
self.__dict__ == other.__dict__)
def __repr__(self):
return 'Match(%s)' % self.ctx
[docs]class Pattern(object):
"""
Base Pattern class. Abstracts the behavior common to all pattern types,
such as name bindings, conditionals and operator overloading for combining
several patterns.
"""
def __init__(self):
self.bound_name = None
self.condition = None
[docs] def match(self, other, ctx=None):
"""
Match this pattern against an object. Operator: `<<`.
:param other: the object this pattern should be matched against.
:param ctx: optional context. If none, an empty one will be
automatically created.
:type ctx: dict
:returns: a :class:`Match` if successful, `None` otherwise.
"""
match = self._does_match(other, ctx)
if match:
ctx = match.ctx
value = match.value or other
if self.bound_name:
if ctx is None:
ctx = {}
try:
previous = ctx[self.bound_name]
if previous != value:
return None
except KeyError:
ctx[self.bound_name] = value
if self.condition is None or self.condition(**ctx):
return Match(ctx)
return None
def __lshift__(self, other):
return self.match(other)
[docs] def bind(self, name):
"""Bind this pattern to the given name. Operator: `%`."""
self.bound_name = name
return self
def __mod__(self, name):
return self.bind(name)
[docs] def if_(self, condition):
"""
Add a boolean condition to this pattern. Operator: `/`.
:param condition: must accept the match context as keyword
arguments and return a boolean-ish value.
:type condition: callable
"""
self.condition = condition
return self
def __div__(self, condition):
return self.if_(condition)
def __truediv__(self, condition):
return self.if_(condition)
[docs] def multiply(self, n):
"""
Build a :class:`ListPattern` that matches `n` instances of this pattern.
Operator: `*`.
Example:
>>> p = EqualsPattern(1).multiply(3)
>>> p.match((1, 1, 1))
Match({})
"""
return build(*([self]*n))
def __mul__(self, length):
return self.multiply(length)
def __rmul__(self, length):
return self.multiply(length)
[docs] def or_with(self, other):
"""
Build a new :class:`OrPattern` with this or the other pattern.
Operator: `|`.
Example:
>>> p = EqualsPattern(1).or_with(InstanceOfPattern(str))
>>> p.match('hello')
Match({})
>>> p.match(1)
Match({})
>>> p.match(2)
"""
patterns = []
for pattern in (self, other):
if isinstance(pattern, OrPattern):
patterns.extend(pattern.patterns)
else:
patterns.append(pattern)
return OrPattern(*patterns)
def __or__(self, other):
return self.or_with(other)
[docs] def head_tail_with(self, other):
"""
Head-tail concatenate this pattern with the other. The lhs pattern will
be the head and the other will be the tail. Operator: `+`.
Example:
>>> p = InstanceOfPattern(int).head_tail_with(ListPattern())
>>> p.match([1])
Match({})
>>> p.match([1, 2])
"""
return ListPattern(self, other)
def __add__(self, other):
return self.head_tail_with(other)
def __eq__(self, other):
return (self.__class__ == other.__class__ and
self.__dict__ == other.__dict__)
def __repr__(self):
return '%s(%s)' % (self.__class__.__name__,
', '.join('='.join((str(k), repr(v))) for (k, v) in
self.__dict__.items() if v))
[docs]class AnyPattern(Pattern):
"""Pattern that matches anything."""
def _does_match(self, other, ctx):
return Match(ctx)
[docs]class EqualsPattern(Pattern):
"""Pattern that only matches objects that equal the given object."""
def __init__(self, obj):
super(EqualsPattern, self).__init__()
self.obj = obj
def _does_match(self, other, ctx):
if self.obj == other:
return Match(ctx)
else:
return None
[docs]class InstanceOfPattern(Pattern):
"""Pattern that only matches instances of the given class."""
def __init__(self, cls):
super(InstanceOfPattern, self).__init__()
self.cls = cls
def _does_match(self, other, ctx):
if isinstance(other, self.cls):
return Match(ctx)
else:
return None
_CompiledRegex = type(re.compile(''))
[docs]class RegexPattern(Pattern):
"""Pattern that only matches strings that match the given regex."""
def __init__(self, regex):
super(RegexPattern, self).__init__()
if not isinstance(regex, _CompiledRegex):
regex = re.compile(regex)
self.regex = regex
def _does_match(self, other, ctx):
re_match = self.regex.match(other)
if re_match:
return Match(ctx, re_match.groups())
return None
[docs]class ListPattern(Pattern):
"""Pattern that only matches iterables whose head matches `head_pattern` and
whose tail matches `tail_pattern`"""
def __init__(self, head_pattern=None, tail_pattern=None):
super(ListPattern, self).__init__()
if head_pattern is not None and tail_pattern is None:
tail_pattern = ListPattern()
self.head_pattern = head_pattern
self.tail_pattern = tail_pattern
def head_tail_with(self, other):
return ListPattern(self.head_pattern,
self.tail_pattern.head_tail_with(other))
def _does_match(self, other, ctx):
try:
if (self.head_pattern is None and
self.tail_pattern is None and
len(other) == 0):
return Match(ctx)
except TypeError:
return None
if isinstance(other, _basestring):
return None
try:
head, tail = other[0], other[1:]
except (IndexError, TypeError):
return None
if self.head_pattern is not None:
match = self.head_pattern.match(head, ctx)
if match:
ctx = match.ctx
match = self.tail_pattern.match(tail, ctx)
if match:
ctx = match.ctx
else:
return None
else:
return None
else:
if len(other):
return None
return Match(ctx)
[docs]class NamedTuplePattern(Pattern):
"""Pattern that only matches named tuples of the given class and whose
contents match the given patterns."""
def __init__(self, casecls, *initpatterns):
super(NamedTuplePattern, self).__init__()
self.casecls_pattern = InstanceOfPattern(casecls)
if (len(initpatterns) == 1 and
isinstance(initpatterns[0], ListPattern)):
self.initargs_pattern = initpatterns[0]
else:
self.initargs_pattern = build(*initpatterns, **dict(is_list=True))
def _does_match(self, other, ctx):
match = self.casecls_pattern.match(other, ctx)
if not match:
return None
ctx = match.ctx
return self.initargs_pattern.match(other, ctx)
[docs]class OrPattern(Pattern):
"""Pattern that matches whenever any of the inner patterns match."""
def __init__(self, *patterns):
if len(patterns) < 2:
raise ValueError('need at least two patterns')
super(OrPattern, self).__init__()
self.patterns = patterns
def _does_match(self, other, ctx):
for pattern in self.patterns:
if ctx is not None:
ctx_ = ctx.copy()
else:
ctx_ = None
match = pattern.match(other, ctx_)
if match:
return match
return None
[docs]def build(*args, **kwargs):
"""
Shorthand pattern factory.
Examples:
>>> build() == AnyPattern()
True
>>> build(1) == EqualsPattern(1)
True
>>> build('abc') == EqualsPattern('abc')
True
>>> build(str) == InstanceOfPattern(str)
True
>>> build(re.compile('.*')) == RegexPattern('.*')
True
>>> build(()) == build([]) == ListPattern()
True
>>> build([1]) == build((1,)) == ListPattern(EqualsPattern(1),
... ListPattern())
True
>>> build(int, str, 'a') == ListPattern(InstanceOfPattern(int),
... ListPattern(InstanceOfPattern(str),
... ListPattern(EqualsPattern('a'))))
True
>>> try:
... from collections import namedtuple
... MyTuple = namedtuple('MyTuple', 'a b c')
... build(MyTuple(1, 2, 3)) == NamedTuplePattern(MyTuple, 1, 2, 3)
... except ImportError:
... True
True
"""
arglen = len(args)
if arglen > 1:
head, tail = args[0], args[1:]
return ListPattern(build(head), build(*tail, **(dict(is_list=True))))
if arglen == 0:
return AnyPattern()
(arg,) = args
if kwargs.get('is_list', False):
return ListPattern(build(arg))
if isinstance(arg, Pattern):
return arg
if isinstance(arg, _CompiledRegex):
return RegexPattern(arg)
if isinstance(arg, tuple) and hasattr(arg, '_fields'):
return NamedTuplePattern(arg.__class__, *map(build, arg))
if isinstance(arg, type):
return InstanceOfPattern(arg)
if isinstance(arg, (tuple, list)):
if len(arg) == 0:
return ListPattern()
return build(*arg, **(dict(is_list=True)))
return EqualsPattern(arg)