import re
from abc import ABC, abstractmethod
[docs]class StreamMatcher(ABC):
"""
Abstract base class for stream matchers.
"""
[docs] @abstractmethod
def match(self, item):
"""
Return ``True`` if the matcher matches an item, otherwise ``False``.
"""
[docs] @abstractmethod
def args_str(self):
"""
Return an args string for the repr.
"""
def __call__(self, item):
return self.match(item)
def __str__(self):
return '{}({})'.format(type(self).__name__, self.args_str())
def __repr__(self):
return str(self)
[docs]def to_matcher(matcher_factory, obj):
"""
Turn an object into a :class:`StreamMatcher` unless it already is one.
:param matcher_factory: A callable capable of turning `obj` into a
:class:`StreamMatcher`.
:param obj: A :class:`StreamMatcher` or an object to turn into one.
:returns: :class:`StreamMatcher`
"""
return obj if isinstance(obj, StreamMatcher) else matcher_factory(obj)
[docs]class CombinationMatcher(StreamMatcher):
"""
Matcher that combines multiple input matchers.
"""
def __init__(self, *matchers):
self._matchers = matchers
[docs] @classmethod
def by_equality(cls, *expected_items):
"""
Construct an instance of this combination matcher from a list of
expected items and/or StreamMatcher instances.
"""
return cls(*(to_matcher(EqualsMatcher, i) for i in expected_items))
[docs] @classmethod
def by_regex(cls, *patterns):
"""
Construct an instance of this combination matcher from a list of
regex patterns and/or StreamMatcher instances.
"""
return cls(*(to_matcher(RegexMatcher, p) for p in patterns))
[docs]class OrderedMatcher(CombinationMatcher):
"""
Matcher that takes a list of matchers, and uses one after the next after
each has a successful match. Returns True ("matches") on the final match.
**Note:** This is a *stateful* matcher. Once it has done its matching,
you'll need to create a new instance.
"""
def __init__(self, *matchers):
super().__init__(*matchers)
self._position = 0
[docs] def match(self, item):
"""
Return ``True`` if the expected matchers are matched in the expected
order, otherwise ``False``.
"""
if self._position == len(self._matchers):
raise RuntimeError('Matcher exhausted, no more matchers to use')
matcher = self._matchers[self._position]
if matcher(item):
self._position += 1
if self._position == len(self._matchers):
# All patterns have been matched
return True
return False
[docs] def args_str(self):
"""
Return an args string for the repr.
"""
matched = [str(m) for m in self._matchers[:self._position]]
unmatched = [str(m) for m in self._matchers[self._position:]]
return 'matched=[{}], unmatched=[{}]'.format(
', '.join(matched), ', '.join(unmatched))
[docs]class UnorderedMatcher(CombinationMatcher):
"""
Matcher that takes a list of matchers, and matches each one to an item.
Each item is tested against each unmatched matcher until a match is found
or all unmatched matchers are checked. Returns True ("matches") on the
final match.
.. note::
This is a *stateful* matcher. Once it has done its matching,
you'll need to create a new instance.
"""
def __init__(self, *matchers):
super().__init__(*matchers)
self._used_matchers = []
@property
def _unused_matchers(self):
return [m for m in self._matchers if m not in self._used_matchers]
[docs] def match(self, item):
"""
Return ``True`` if the expected matchers are matched in any order,
otherwise ``False``.
"""
if not self._unused_matchers:
raise RuntimeError('Matcher exhausted, no more matchers to use')
for matcher in self._unused_matchers:
if matcher(item):
self._used_matchers.append(matcher)
break
if not self._unused_matchers:
# All patterns have been matched
return True
return False
[docs] def args_str(self):
"""
Return an args string for the repr.
"""
matched = [str(m) for m in self._used_matchers]
unmatched = [str(m) for m in self._unused_matchers]
return 'matched=[{}], unmatched=[{}]'.format(
', '.join(matched), ', '.join(unmatched))
[docs]class EqualsMatcher(StreamMatcher):
"""
Matcher that matches items by equality.
"""
def __init__(self, expected_item):
self._expected_item = expected_item
[docs] def match(self, item):
"""
Return ``True`` if the item matches the expected value exactly,
otherwise ``False``.
"""
return item == self._expected_item
[docs] def args_str(self):
"""
Return an args string for the repr.
"""
return repr(self._expected_item)
[docs]class RegexMatcher(StreamMatcher):
"""
Matcher that matches items by regex pattern.
"""
def __init__(self, pattern):
self._regex = re.compile(pattern)
[docs] def match(self, item):
"""
Return ``True`` if the item matches the expected regex, otherwise
``False``.
"""
return self._regex.search(item) is not None
[docs] def args_str(self):
"""
Return an args string for the repr.
"""
return repr(self._regex.pattern)