# yapf
"""
The Match module
~~~~~~~~~~~~~~~~
This module implements a technique for pattern matching.
"""
import os
from typing import (Any, Type, Iterable, Tuple, Mapping, Callable, Union, Set,
List, Dict, Collection, Sequence, TypeVar, Generic, cast)
from . import lang
from kingston import decl
from kingston.decl import box, unbox, Singular
from kingston import xxx_kind as kind
from kingston.xxx_kind import primparams, xrtype # type: ignore[attr-defined]
SingularTypeCand = Type[Any]
ComplexTypeCand = Iterable[SingularTypeCand]
TypePatternCand = Union[Singular, ComplexTypeCand]
PatternCand = Union[decl.Singular, Iterable[decl.Singular]]
FoundPattern = Union[PatternCand, object, Collection[decl.Singular]]
MatchFunc = Callable[[Any, Any], bool]
Plural = Union[Set[Any], List[Any], Tuple[Any, ...], Dict[Any, Any]]
Dualism = Union[Singular, Plural]
[docs]class Conflict(TypeError):
"""Exception raised if a pattern to be matched already have been
applied to a `Matcher` instance.
"""
[docs]class Mismatch(ValueError):
"Exception to signal matching error in Matcher objects."
[docs]class Miss:
"Symbol for a missed match."
[docs]class NoNextValue:
"Symbol signifying that no more values are available to pattern check for."
[docs]class NoNextAnchor:
"Symbol signifying that no more anchor values exist in a pattern."
[docs]def match(cand: Any, pattern: Any) -> bool:
"""*”Primitive”* function that checks an individual value against
another. Checking against ``Any`` works as a wildcard and will
always result in ``True``.
"""
cand = kind.cast_to_hashable(cand)
pattern = kind.cast_to_hashable(pattern)
accepted = {cand, Any}
return True if pattern in accepted else False
peek_nv = lang.infinite_item(1, NoNextValue) # type: ignore
peek_na = lang.infinite_item(1, NoNextAnchor) # type: ignore
[docs]def move(left: Sequence, pattern: Sequence, matchfn: Callable = match):
# def move(left: Sequence, pattern: Sequence) -> Tuple[SeqOrMiss, SeqOrMiss]:
"""One step of the pattern matching process. The ``move()`` function
will take to sequences (``left``, ``pattern``) that represents the
current state of matching and produce a tuple representing the
next (``left``, ``pattern``) pair of the pattern matching.
:param left: Values that haven't been matched yet.
:param pattern: Pattern values to match subsequently.
:param matchfn: Function that should compare a pair of values.
:return: A pair representing the next step in the matching process.
:rtype: Tuple[Sequence,Sequence]
"""
VC, AC = len(left), len(pattern)
lensum = VC + AC
if VC == 0 or AC == 0:
return Miss, Miss
if pattern == (..., ):
return (), ()
if type(left) != type(pattern):
return Miss, Miss
V, A = left[0], pattern[0]
if lensum == 2 and matchfn(V, A):
return (), ()
elif lensum == 2 and not matchfn(V, A):
# abandon
return Miss, Miss
elif lensum > 2:
if matchfn(V, A):
# advance
return left[1:], pattern[1:]
elif A is ...:
NV, NA = peek_nv(left), peek_na(pattern)
if matchfn(NV, NA):
# advance
return left[1:], pattern[1:]
else:
# drag / advance
if VC == 1:
# last element -> unload (drop ...)
# 0
return left, pattern[1:]
elif VC > 1:
# several elements -> drag (drop one value, keep ...)
return left[1:], pattern
else:
# abandon
return Miss, Miss
[docs]def matches(values: Sequence,
patterns: Sequence,
matchfn: Callable = match) -> Union[Sequence, Type[Miss]]:
"""Tries to match ``values`` from ``patterns``.
:param values: A sequence of values to match.
:param patterns: A sequence of patterns that may match ``values``.
:return: The pattern that was matched or ``Miss``.
:rtype: Union[Sequence, Type[Miss]]
"""
for pattern in box(patterns):
# Operate on copies ->
matched, pending = box(values)[:], box(pattern)[:]
while matched or pending:
# (-> comsumes the copies)
matched, pending = move(matched, pending, matchfn)
if matched is Miss:
break
if matched is Miss:
continue
else:
return pattern
return Miss
def resolve_pattern(params: Any, opts: Any) -> TypePatternCand:
safeboxed = box(unbox(params))
return safeboxed if len(opts) == 0 else (*safeboxed, Mapping)
MatchArgT = TypeVar('MatchArgT')
MatchRetT = TypeVar('MatchRetT')
[docs]class Matcher(dict, Generic[MatchArgT, MatchRetT]):
"""Common base for all matcher classes.
Since ``Matcher`` is also ``Generic``, you use it to subtype
concrete instances of matchers you implement.
"""
@staticmethod
def signature(handler: Callable) -> Sequence: # pragma: nocov
...
def callsign(self, args: Sequence[MatchArgT],
kwargs: Mapping[Any, Any]) -> Sequence: # pragma: nocov
...
def _raise_on_conflict(self, dispatch):
try:
conflicting = self[dispatch]
raise Conflict(f'Pattern {dispatch} had a previous conflict '
f'{dispatch}={conflicting}')
except KeyError:
pass
def case(self, handler: Callable) -> Callable:
dispatch = self.signature(handler)
self._raise_on_conflict(dispatch)
self[dispatch] = handler
return handler
def missed(self, handler: Callable) -> Callable:
self[Miss] = handler
return handler
def match(self, args: Sequence, kwargs: Mapping) -> Callable:
cand = self.callsign(args, kwargs)
key = matches(cand, tuple(self))
return self[key]
def invoke(self, handler: Callable, args: Sequence, kwargs: Mapping):
return handler() if lang.arity(handler) == 0 else handler(
*box(unbox(args)), **kwargs)
def __call__(self, *args: Any, **kwargs: Any) -> MatchRetT:
for cb, deco_case in ((getattr(self, name), getattr(self,
name).__case__)
for name in dir(self)
if hasattr(getattr(self, name), '__case__')):
self[deco_case] = cb
try:
return self.invoke(self.match(args, kwargs), args, kwargs)
except KeyError:
try:
return self.invoke(self[Miss], args, kwargs)
except KeyError as exc:
raise Mismatch(f"Mismatched ({args!r}, {kwargs!r})")
def explain(self, out=False): # pragma: nocov
"""Development convenience tool -
creates a summary of what patterns matcher object contain
and which functions the matchings map to.
"""
lines = (f"- A {self.__class__.__name__}", )
for matching in self:
fn = self[matching]
lines = (*lines, f" - {matching!r} : {kind.nick(fn)}")
text = os.linesep.join(lines)
if out:
print(text)
else:
return text
[docs]class TypeMatcher(Matcher):
"""Concrete implementation of a type matcher instance.
If you want to type a type matcher, use standard technique when
using ``Generic`` types:
>>> from kingston.match import Matcher, TypeMatcher
>>> my_int_matcher:Matcher[int, int] = TypeMatcher({
... int: lambda x: x+1,
... str: lambda x: 'str'})
>>> my_int_matcher(10)
11
>>> my_int_matcher(20)
21
>>> my_int_matcher('foo') # ok at runtime but fails mypy
'str'
>>>
You can also subclass type matchers and use a decorator to declare
cases as methods:
>>> from kingston.match import Matcher, TypeMatcher, case
>>> from numbers import Number
>>> class NumberDescriber(TypeMatcher):
... @case
... def describe_one_int(self, one:int) -> str:
... return "One integer"
...
... @case
... def describe_two_ints(self, one:int, two:int) -> str:
... return "Two integers"
...
... @case
... def describe_one_float(self, one:float) -> str:
... return "One float"
>>> my_num_matcher:Matcher[Number, str] = NumberDescriber()
>>> my_num_matcher(1)
'One integer'
>>> my_num_matcher(1, 2)
'Two integers'
>>> my_num_matcher(1.0)
'One float'
>>>
"""
@staticmethod
def signature(handler: Callable) -> Sequence:
return cast(Sequence, unbox(primparams(handler)))
def match(self, args: Sequence, kwargs: Mapping) -> Callable:
try:
return super(TypeMatcher, self).match(args, kwargs)
except KeyError:
cand = self.callsign(args, kwargs)
key = matches(cand, tuple(self), issubclass)
return self[key]
def callsign(self, args: Sequence[MatchArgT],
kwargs: Mapping[Any, Any]) -> Sequence:
return cast(Sequence[Any], xrtype(resolve_pattern(args, kwargs)))
[docs]class ValueMatcher(Matcher):
"""Concrete implementation of a value matching instance.
If you want to type a type matcher, use standard technique when
using ``Generic`` types:
>>> from kingston.match import ValueMatcher, Miss
>>> my_val_matcher:Matcher[int, str] = ValueMatcher({
... 1: lambda x: 'one!',
... 2: lambda x: 'two!',
... Miss: lambda x: 'many!'})
>>> my_val_matcher(1)
'one!'
>>> my_val_matcher(2)
'two!'
>>> my_val_matcher(3)
'many!'
>>> my_val_matcher('x') # ok at runtime but fails mypy (& missleading..)
'many!'
>>>
You can also declare cases as methods in a custom ``ValueMatcher``
subclass.
Use the function ``value_case()`` to declare value
cases. **Note:** *imported as a shorthand*:
>>> from kingston.match import Matcher, ValueMatcher
>>> from kingston.match import value_case as case
>>> class SimplestEval(ValueMatcher):
... @case(Any, '+', Any)
... def _add(self, a, op, b) -> int:
... return a + b
...
... @case(Any, '-', Any)
... def _sub(self, a, op, b) -> int:
... return a - b
>>> simpl_eval = SimplestEval()
>>> simpl_eval(1, '+', 2)
3
>>> simpl_eval(10, '-', 5)
5
"""
def callsign(self, args: Sequence[MatchArgT],
kwargs: Mapping[Any, Any]) -> Sequence:
return cast(Sequence[Any], unbox(resolve_pattern(args, kwargs)))
def case(self, *params: Any, **opts: Any) -> Callable:
"""Decorator to add a function. The types of the parameters. The types
that will be matched is taken from the signature of the
decorated function.
"""
def wrap(handler, *xparams, **xopts):
dispatch = unbox(params)
self._raise_on_conflict(dispatch)
self[dispatch] = handler
return handler
return wrap
def type_case(func: Callable) -> Callable:
func.__case__ = TypeMatcher.signature(func)[1:]
return func
# Note: Guess based on what I personally use most.
case = type_case
def value_case(*values: Any) -> Callable:
def wrap(func: Callable):
func.__case__ = values
return func
return wrap