Source code for kingston.match

# yapf
"""
The Match module
~~~~~~~~~~~~~~~~

This module implements a technique for pattern matching.


IDEA: Statement-oriented matcher within the same scope:
with ValueMatcher('sum', 1, 2, 3) as m:
    with m.case('sum', Any, Any, Any) as _, one, two, three:
        print(one + two + three)
    with m.case('avg', ...) as _, *values:
        print(sum(*values))
    with.m.miss() as *missed:
        print('MISS!')
"""

import os

from typing import (Any, Type, Iterable, Tuple, Mapping, Callable, Union, Set,
                    List, Dict, Collection, Sequence, TypeVar, Generic, cast)

import funcy as fy  # type: ignore[import]

from . import lang
from . import decl
from .decl import box, unbox, Singular
from . import xxx_kind as kind
from .xxx_kind import funcnick  # type: ignore[attr-defined]
from .xxx_kind import primparams  # type: ignore[attr-defined]
from .xxx_kind import 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."
class Miss_: "Symbol for a missed match." Miss = (Miss_, ) # Miss constant, a marker for missed matches
[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. The ``match()`` function is *only* responsible for checking two values, matching markers `Any` and `Ellipsis` are handled elsewhere. """ cand, pattern = kind.cast_to_hashable(cand), kind.cast_to_hashable(pattern) return cand == pattern
def match_subtype(cand: Any, pattern: Any) -> bool: if fy.is_seqcoll(cand): return issubclass(type(cand), pattern) else: return issubclass(cand, pattern) peek1 = lang.itempadded(1, NoNextValue) # type: ignore[attr-defined]
[docs]def move(matched: Sequence, pending: Sequence, matchfn: Callable = match) -> Tuple[Sequence, Sequence]: if type(matched) != type(pending): return Miss, Miss n_matched, n_pending = len(matched), len(pending) if 0 in {n_matched, n_pending}: return Miss, Miss value, against = matched[0], pending[0] if against is Any: # forward return matched[1:], pending[1:] elif against is ... and n_matched > 1: anchor = peek1(pending) if anchor is not NoNextValue and matchfn(matched[1], anchor): # drag/unload return matched[min(n_matched, 2):], pending[min(n_pending, 2):] else: # drag return matched[1:], pending elif pending == (..., ): return (), () elif against is ...: return Miss, Miss elif matchfn(value, against): # forward return matched[1:], pending[1:] # attempt 1 else: return Miss, Miss
[docs]def matches(values: Sequence, patterns: Sequence, matchfn: Callable = match) -> Sequence: """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') DecoratorCases = Tuple[Callable[..., Any], Sequence[Any]]
[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. """ __case__: DecoratorCases @staticmethod def signature( handler: Callable ) -> Tuple[Callable[..., Any], Sequence[Any]]: # 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: handler = self.match(args, kwargs) return self.invoke(handler, args, kwargs) except KeyError: try: return self.invoke(self[Miss], args, kwargs) except KeyError: 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 def responserep(self, rep: Union[Type, Sequence[Type]], nickfunc: Callable) -> str: if fy.is_seqcoll(rep): return ','.join(map(nickfunc, cast(Sequence, rep))) else: return nickfunc(rep) def descresponses(self, nickfunc: Callable) -> str: return ', '.join( f"({self.responserep(key, nickfunc)})->{funcnick(resp)}" for key, resp in self.items())
[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' >>> It will try to give a reasonably human representation when inspected: >>> my_int_matcher <TypeMatcher: (int)->λ, (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) -> Tuple[Callable[..., Any], Sequence[Any]]: return cast(Tuple[Callable[..., Any], Sequence[Any]], 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), match_subtype) return self[key] def callsign(self, args: Sequence[MatchArgT], kwargs: Mapping[Any, Any]) -> Sequence: return cast(Sequence[Any], xrtype(resolve_pattern(args, kwargs))) def __repr__(self) -> str: nickfunc = kind.typenick # type: ignore[attr-defined] matchreps = self.descresponses(nickfunc) return f"<TypeMatcher: {matchreps} >"
[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!' >>> It will try to give a reasonably human representation when inspected: >>> my_val_matcher <ValueMatcher: (1)->λ, (2)->λ, (<class 'kingston.match.Miss_'>)->λ > >>> 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 __repr__(self) -> str: matchreps = self.descresponses(str) return f"<ValueMatcher: {matchreps} >"
def type_case(func: TypeMatcher) -> Callable: func.__case__ = cast(DecoratorCases, 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: ValueMatcher): func.__case__ = cast(DecoratorCases, values) return func return wrap