Hello community, here is the log from the commit of package python-pampy for openSUSE:Factory checked in at 2020-03-11 18:55:36 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pampy (Old) and /work/SRC/openSUSE:Factory/.python-pampy.new.3160 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pampy" Wed Mar 11 18:55:36 2020 rev:2 rq:783882 version:0.3.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pampy/python-pampy.changes 2019-01-24 14:11:26.763498723 +0100 +++ /work/SRC/openSUSE:Factory/.python-pampy.new.3160/python-pampy.changes 2020-03-11 18:56:46.759713324 +0100 @@ -1,0 +2,9 @@ +Wed Mar 11 14:44:24 UTC 2020 - Marketa Calabkova <mcalabk...@suse.com> + +- update to version 0.3.0 + * Add type annotations support for matching + * Add support for callables which return Tuple[bool, List] + * Make match_value not depend on itself and add datetime example + * Add Enum support for matching + +------------------------------------------------------------------- Old: ---- pampy-0.2.1.tar.gz New: ---- pampy-0.3.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pampy.spec ++++++ --- /var/tmp/diff_new_pack.B4YgkH/_old 2020-03-11 18:56:47.143713496 +0100 +++ /var/tmp/diff_new_pack.B4YgkH/_new 2020-03-11 18:56:47.147713498 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-pampy # -# Copyright (c) 2019 SUSE LINUX GmbH, Nuernberg, Germany. +# Copyright (c) 2020 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -19,7 +19,7 @@ %define skip_python2 1 %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-pampy -Version: 0.2.1 +Version: 0.3.0 Release: 0 Summary: An alternate pattern matching for Python License: MIT ++++++ pampy-0.2.1.tar.gz -> pampy-0.3.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pampy-0.2.1/PKG-INFO new/pampy-0.3.0/PKG-INFO --- old/pampy-0.2.1/PKG-INFO 2018-12-24 15:08:24.000000000 +0100 +++ new/pampy-0.3.0/PKG-INFO 2019-11-07 16:50:51.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: pampy -Version: 0.2.1 +Version: 0.3.0 Summary: The Pattern Matching for Python you always dreamed of Home-page: https://github.com/santinic/pampy Author: Claudio Santini @@ -162,6 +162,86 @@ match(pet, Pet(_, _), lambda name, age: (name, age)) # => ('rover', 7) ``` + ## Using typing + Pampy supports typing annotations. + + ```python + + class Pet: pass + class Dog(Pet): pass + class Cat(Pet): pass + class Hamster(Pet): pass + + timestamp = NewType("year", Union[int, float]) + + def annotated(a: Tuple[int, float], b: str, c: E) -> timestamp: + pass + + match((1, 2), Tuple[int, int], lambda a, b: (a, b)) # => (1, 2) + match(1, Union[str, int], lambda x: x) # => 1 + match('a', Union[str, int], lambda x: x) # => 'a' + match('a', Optional[str], lambda x: x) # => 'a' + match(None, Optional[str], lambda x: x) # => None + match(Pet, Type[Pet], lambda x: x) # => Pet + match(Cat, Type[Pet], lambda x: x) # => Cat + match(Dog, Any, lambda x: x) # => Dog + match(Dog, Type[Any], lambda x: x) # => Dog + match(15, timestamp, lambda x: x) # => 15 + match(10.0, timestamp, lambda x: x) # => 10.0 + match([1, 2, 3], List[int], lambda x: x) # => [1, 2, 3] + match({'a': 1, 'b': 2}, Dict[str, int], lambda x: x) # => {'a': 1, 'b': 2} + match(annotated, + Callable[[Tuple[int, float], str, Pet], timestamp], lambda x: x + ) # => annotated + ``` + For iterable generics actual type of value is guessed based on the first element. + ```python + match([1, 2, 3], List[int], lambda x: x) # => [1, 2, 3] + match([1, "b", "a"], List[int], lambda x: x) # => [1, "b", "a"] + match(["a", "b", "c"], List[int], lambda x: x) # raises MatchError + match(["a", "b", "c"], List[Union[str, int]], lambda x: x) # ["a", "b", "c"] + + match({"a": 1, "b": 2}, Dict[str, int], lambda x: x) # {"a": 1, "b": 2} + match({"a": 1, "b": "dog"}, Dict[str, int], lambda x: x) # {"a": 1, "b": "dog"} + match({"a": 1, 1: 2}, Dict[str, int], lambda x: x) # {"a": 1, 1: 2} + match({2: 1, 1: 2}, Dict[str, int], lambda x: x) # raises MatchError + match({2: 1, 1: 2}, Dict[Union[str, int], int], lambda x: x) # {2: 1, 1: 2} + ``` + Iterable generics also match with any of their subtypes. + ```python + match([1, 2, 3], Iterable[int], lambda x: x) # => [1, 2, 3] + match({1, 2, 3}, Iterable[int], lambda x: x) # => {1, 2, 3} + match(range(10), Iterable[int], lambda x: x) # => range(10) + + match([1, 2, 3], List[int], lambda x: x) # => [1, 2, 3] + match({1, 2, 3}, List[int], lambda x: x) # => raises MatchError + match(range(10), List[int], lambda x: x) # => raises MatchError + + match([1, 2, 3], Set[int], lambda x: x) # => raises MatchError + match({1, 2, 3}, Set[int], lambda x: x) # => {1, 2, 3} + match(range(10), Set[int], lambda x: x) # => raises MatchError + ``` + For Callable any arg without annotation treated as Any. + ```python + def annotated(a: int, b: int) -> float: + pass + + def not_annotated(a, b): + pass + + def partially_annotated(a, b: float): + pass + + match(annotated, Callable[[int, int], float], lambda x: x) # => annotated + match(not_annotated, Callable[[int, int], float], lambda x: x) # => raises MatchError + match(not_annotated, Callable[[Any, Any], Any], lambda x: x) # => not_annotated + match(annotated, Callable[[Any, Any], Any], lambda x: x) # => raises MatchError + match(partially_annotated, + Callable[[Any, float], Any], lambda x: x + ) # => partially_annotated + ``` + TypeVar is not supported. + ## All the things you can match As Pattern you can use any Python type, any class, or any Python value. @@ -191,6 +271,15 @@ | `{'type':'dog', age: int }` | Any dict with `type: "dog"` and with an `int` age | `{"type":"dog", "age": 3}` | `3` | `{"type":"dog", "age":2.3}` | | `re.compile('(\w+)-(\w+)-cat$')` | Any string that matches that regular expression expr | `"my-fuffy-cat"` | `"my"` and `"puffy"` | `"fuffy-dog"` | | `Pet(name=_, age=7)` | Any Pet dataclass with `age == 7` | `Pet('rover', 7)` | `['rover']` | `Pet('rover', 8)` | + | `Any` | The same as `_` | | that value | | + | `Union[int, float, None]` | Any integer or float number or None | `2.35` | `2.35` | any other value | + | `Optional[int]` | The same as `Union[int, None]` | `2` | `2` | any other value | + | `Type[MyClass]` | Any subclass of MyClass. **And any class that extends MyClass.** | `MyClass` | that class | any other object | + | `Callable[[int], float]` | Any callable with exactly that signature | `def a(q:int) -> float: ...` | that function | `def a(q) -> float: ...` | + | `Tuple[MyClass, int, float]` | The same as `(MyClass, int, float)` | | | | + | `Mapping[str, int]` Any subtype of `Mapping` acceptable too | any mapping or subtype of mapping with string keys and integer values | `{'a': 2, 'b': 3}` | that dict | `{'a': 'b', 'b': 'c'}` | + | `Iterable[int]` Any subtype of `Iterable` acceptable too | any iterable or subtype of iterable with integer values | `range(10)` and `[1, 2, 3]` | that iterable | `['a', 'b', 'v']` | + ## Using default @@ -234,7 +323,7 @@ or ```$ pip3 install pampy``` - ## If you really must to use Python2 + ## If you really must use Python2 Pampy is Python3-first, but you can use most of its features in Python2 via [this backport](https://pypi.org/project/backports.pampy/) by Manuel Barkhau: ```pip install backports.pampy``` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pampy-0.2.1/README.md new/pampy-0.3.0/README.md --- old/pampy-0.2.1/README.md 2018-12-24 14:50:14.000000000 +0100 +++ new/pampy-0.3.0/README.md 2019-07-13 13:23:08.000000000 +0200 @@ -154,6 +154,86 @@ match(pet, Pet(_, _), lambda name, age: (name, age)) # => ('rover', 7) ``` +## Using typing +Pampy supports typing annotations. + +```python + +class Pet: pass +class Dog(Pet): pass +class Cat(Pet): pass +class Hamster(Pet): pass + +timestamp = NewType("year", Union[int, float]) + +def annotated(a: Tuple[int, float], b: str, c: E) -> timestamp: + pass + +match((1, 2), Tuple[int, int], lambda a, b: (a, b)) # => (1, 2) +match(1, Union[str, int], lambda x: x) # => 1 +match('a', Union[str, int], lambda x: x) # => 'a' +match('a', Optional[str], lambda x: x) # => 'a' +match(None, Optional[str], lambda x: x) # => None +match(Pet, Type[Pet], lambda x: x) # => Pet +match(Cat, Type[Pet], lambda x: x) # => Cat +match(Dog, Any, lambda x: x) # => Dog +match(Dog, Type[Any], lambda x: x) # => Dog +match(15, timestamp, lambda x: x) # => 15 +match(10.0, timestamp, lambda x: x) # => 10.0 +match([1, 2, 3], List[int], lambda x: x) # => [1, 2, 3] +match({'a': 1, 'b': 2}, Dict[str, int], lambda x: x) # => {'a': 1, 'b': 2} +match(annotated, + Callable[[Tuple[int, float], str, Pet], timestamp], lambda x: x +) # => annotated +``` +For iterable generics actual type of value is guessed based on the first element. +```python +match([1, 2, 3], List[int], lambda x: x) # => [1, 2, 3] +match([1, "b", "a"], List[int], lambda x: x) # => [1, "b", "a"] +match(["a", "b", "c"], List[int], lambda x: x) # raises MatchError +match(["a", "b", "c"], List[Union[str, int]], lambda x: x) # ["a", "b", "c"] + +match({"a": 1, "b": 2}, Dict[str, int], lambda x: x) # {"a": 1, "b": 2} +match({"a": 1, "b": "dog"}, Dict[str, int], lambda x: x) # {"a": 1, "b": "dog"} +match({"a": 1, 1: 2}, Dict[str, int], lambda x: x) # {"a": 1, 1: 2} +match({2: 1, 1: 2}, Dict[str, int], lambda x: x) # raises MatchError +match({2: 1, 1: 2}, Dict[Union[str, int], int], lambda x: x) # {2: 1, 1: 2} +``` +Iterable generics also match with any of their subtypes. +```python +match([1, 2, 3], Iterable[int], lambda x: x) # => [1, 2, 3] +match({1, 2, 3}, Iterable[int], lambda x: x) # => {1, 2, 3} +match(range(10), Iterable[int], lambda x: x) # => range(10) + +match([1, 2, 3], List[int], lambda x: x) # => [1, 2, 3] +match({1, 2, 3}, List[int], lambda x: x) # => raises MatchError +match(range(10), List[int], lambda x: x) # => raises MatchError + +match([1, 2, 3], Set[int], lambda x: x) # => raises MatchError +match({1, 2, 3}, Set[int], lambda x: x) # => {1, 2, 3} +match(range(10), Set[int], lambda x: x) # => raises MatchError +``` +For Callable any arg without annotation treated as Any. +```python +def annotated(a: int, b: int) -> float: + pass + +def not_annotated(a, b): + pass + +def partially_annotated(a, b: float): + pass + +match(annotated, Callable[[int, int], float], lambda x: x) # => annotated +match(not_annotated, Callable[[int, int], float], lambda x: x) # => raises MatchError +match(not_annotated, Callable[[Any, Any], Any], lambda x: x) # => not_annotated +match(annotated, Callable[[Any, Any], Any], lambda x: x) # => raises MatchError +match(partially_annotated, + Callable[[Any, float], Any], lambda x: x +) # => partially_annotated +``` +TypeVar is not supported. + ## All the things you can match As Pattern you can use any Python type, any class, or any Python value. @@ -183,6 +263,15 @@ | `{'type':'dog', age: int }` | Any dict with `type: "dog"` and with an `int` age | `{"type":"dog", "age": 3}` | `3` | `{"type":"dog", "age":2.3}` | | `re.compile('(\w+)-(\w+)-cat$')` | Any string that matches that regular expression expr | `"my-fuffy-cat"` | `"my"` and `"puffy"` | `"fuffy-dog"` | | `Pet(name=_, age=7)` | Any Pet dataclass with `age == 7` | `Pet('rover', 7)` | `['rover']` | `Pet('rover', 8)` | +| `Any` | The same as `_` | | that value | | +| `Union[int, float, None]` | Any integer or float number or None | `2.35` | `2.35` | any other value | +| `Optional[int]` | The same as `Union[int, None]` | `2` | `2` | any other value | +| `Type[MyClass]` | Any subclass of MyClass. **And any class that extends MyClass.** | `MyClass` | that class | any other object | +| `Callable[[int], float]` | Any callable with exactly that signature | `def a(q:int) -> float: ...` | that function | `def a(q) -> float: ...` | +| `Tuple[MyClass, int, float]` | The same as `(MyClass, int, float)` | | | | +| `Mapping[str, int]` Any subtype of `Mapping` acceptable too | any mapping or subtype of mapping with string keys and integer values | `{'a': 2, 'b': 3}` | that dict | `{'a': 'b', 'b': 'c'}` | +| `Iterable[int]` Any subtype of `Iterable` acceptable too | any iterable or subtype of iterable with integer values | `range(10)` and `[1, 2, 3]` | that iterable | `['a', 'b', 'v']` | + ## Using default @@ -226,7 +315,7 @@ or ```$ pip3 install pampy``` -## If you really must to use Python2 +## If you really must use Python2 Pampy is Python3-first, but you can use most of its features in Python2 via [this backport](https://pypi.org/project/backports.pampy/) by Manuel Barkhau: ```pip install backports.pampy``` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pampy-0.2.1/pampy/__init__.py new/pampy-0.3.0/pampy/__init__.py --- old/pampy-0.2.1/pampy/__init__.py 2018-12-24 15:06:29.000000000 +0100 +++ new/pampy-0.3.0/pampy/__init__.py 2019-07-13 13:30:15.000000000 +0200 @@ -1,4 +1,4 @@ -__version__ = '0.2.1' +__version__ = '0.3.0' import sys diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pampy-0.2.1/pampy/helpers.py new/pampy-0.3.0/pampy/helpers.py --- old/pampy-0.2.1/pampy/helpers.py 2018-12-24 12:08:44.000000000 +0100 +++ new/pampy-0.3.0/pampy/helpers.py 2019-07-13 13:23:08.000000000 +0200 @@ -1,4 +1,17 @@ import inspect +from typing import ( + Union, + Any, + Iterable, + TypeVar, +) + +try: + from typing import GenericMeta +except ImportError: + from typing import _GenericAlias as GenericMeta + +T = TypeVar("T") class UnderscoreType: @@ -62,3 +75,34 @@ return is_dataclass(value) except ImportError: return False + + +def get_extra(pattern): + return getattr(pattern, "__extra__", None) or getattr(pattern, "__origin__", None) + + +def peek(iter_: Iterable[T]) -> T: + return next(iter(iter_)) + + +def is_newtype(pattern): + return inspect.isfunction(pattern) and hasattr(pattern, '__supertype__') + + +def is_generic(pattern): + return isinstance(pattern, GenericMeta) + + +def is_union(pattern): + return isinstance(pattern, Union.__class__) or getattr(pattern, "__origin__", None) == Union + + +def is_typing_stuff(pattern): + return pattern == Any or is_generic(pattern) or is_union(pattern) or is_newtype(pattern) + + +def get_real_type(subtype): + if is_newtype(subtype): + return get_real_type(subtype.__supertype__) + else: + return subtype diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pampy-0.2.1/pampy/pampy.py new/pampy-0.3.0/pampy/pampy.py --- old/pampy-0.2.1/pampy/pampy.py 2018-12-24 12:09:38.000000000 +0100 +++ new/pampy-0.3.0/pampy/pampy.py 2019-07-13 13:23:08.000000000 +0200 @@ -1,10 +1,41 @@ -from collections import Iterable +from collections.abc import ( + Iterable, + Mapping, + Callable as ACallable, +) from itertools import zip_longest -from typing import Tuple, List -from typing import Pattern as RegexPattern - -from pampy.helpers import * +from enum import Enum +from typing import ( + Any, + Generic, + TypeVar, + Tuple, + List, + Pattern as RegexPattern, + Callable, +) +import inspect + +from pampy.helpers import ( + UnderscoreType, + HeadType, + TailType, + get_lambda_args_error_msg, + BoxedArgs, + PaddedValue, + NoDefault, + is_typing_stuff, + is_dataclass, + is_generic, + is_newtype, + is_union, + pairwise, + peek, + get_real_type, + get_extra, +) +T = TypeVar('T') _ = ANY = UnderscoreType() HEAD = HeadType() REST = TAIL = TailType() @@ -28,7 +59,9 @@ def match_value(pattern, value) -> Tuple[bool, List]: if value is PaddedValue: return False, [] - elif isinstance(pattern, (int, float, str, bool)): + elif is_typing_stuff(pattern): + return match_typing_stuff(pattern, value) + elif isinstance(pattern, (int, float, str, bool, Enum)): eq = pattern == value type_eq = type(pattern) == type(value) return eq and type_eq, [] @@ -37,20 +70,21 @@ elif isinstance(pattern, type): if isinstance(value, pattern): return True, [value] - else: - return False, [] elif isinstance(pattern, (list, tuple)): return match_iterable(pattern, value) elif isinstance(pattern, dict): return match_dict(pattern, value) elif callable(pattern): return_value = pattern(value) - if return_value is True: - return True, [value] - elif return_value is False: - pass + + if isinstance(return_value, bool): + return return_value, [value] + elif isinstance(return_value, tuple) and len(return_value) == 2 \ + and isinstance(return_value[0], bool) and isinstance(return_value[1], list): + return return_value else: - raise MatchError("Warning! pattern function %s is not returning a boolean, but instead %s" % + raise MatchError("Warning! pattern function %s is not returning a boolean " + "nor a tuple of (boolean, list), but instead %s" % (pattern, return_value)) elif isinstance(pattern, RegexPattern): rematch = pattern.search(value) @@ -137,7 +171,90 @@ return True, total_extracted +def match_typing_stuff(pattern, value) -> Tuple[bool, List]: + if pattern == Any: + return match_value(ANY, value) + elif is_union(pattern): + for subpattern in pattern.__args__: + is_matched, extracted = match_value(subpattern, value) + if is_matched: + return True, extracted + else: + return False, [] + elif is_newtype(pattern): + return match_value(pattern.__supertype__, value) + elif is_generic(pattern): + return match_generic(pattern, value) + else: + return False, [] + + +def match_generic(pattern: Generic[T], value) -> Tuple[bool, List]: + if get_extra(pattern) == type: # Type[int] for example + real_value = None + if is_newtype(value): + real_value = value + value = get_real_type(value) + if not inspect.isclass(value): + return False, [] + + type_ = pattern.__args__[0] + if type_ == Any: + return True, [real_value or value] + if is_newtype(type_): # NewType case + type_ = get_real_type(type_) + + if issubclass(value, type_): + return True, [real_value or value] + else: + return False, [] + + elif get_extra(pattern) == ACallable: + if callable(value): + spec = inspect.getfullargspec(value) + annotations = spec.annotations + artgtypes = [annotations.get(arg, Any) for arg in spec.args] + ret_type = annotations.get('return', Any) + if pattern == Callable[[*artgtypes], ret_type]: + return True, [value] + else: + return False, [] + else: + return False, [] + + elif get_extra(pattern) == tuple: + return match_value(pattern.__args__, value) + + elif issubclass(get_extra(pattern), Mapping): + type_matched, _captured = match_value(get_extra(pattern), value) + if not type_matched: + return False, [] + k_type, v_type = pattern.__args__ + + key_example = peek(value) + key_matched, _captured = match_value(k_type, key_example) + if not key_matched: + return False, [] + + value_matched, _captured = match_value(v_type, value[key_example]) + if not value_matched: + return False, [] + else: + return True, [value] + elif issubclass(get_extra(pattern), Iterable): + type_matched, _captured = match_value(get_extra(pattern), value) + if not type_matched: + return False, [] + v_type, = pattern.__args__ + v = peek(value) + value_matched, _captured = match_value(v_type, v) + if not value_matched: + return False, [] + else: + return True, [value] + else: + return False, [] def match(var, *args, default=NoDefault, strict=True): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pampy-0.2.1/pampy.egg-info/PKG-INFO new/pampy-0.3.0/pampy.egg-info/PKG-INFO --- old/pampy-0.2.1/pampy.egg-info/PKG-INFO 2018-12-24 15:08:24.000000000 +0100 +++ new/pampy-0.3.0/pampy.egg-info/PKG-INFO 2019-11-07 16:50:51.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: pampy -Version: 0.2.1 +Version: 0.3.0 Summary: The Pattern Matching for Python you always dreamed of Home-page: https://github.com/santinic/pampy Author: Claudio Santini @@ -162,6 +162,86 @@ match(pet, Pet(_, _), lambda name, age: (name, age)) # => ('rover', 7) ``` + ## Using typing + Pampy supports typing annotations. + + ```python + + class Pet: pass + class Dog(Pet): pass + class Cat(Pet): pass + class Hamster(Pet): pass + + timestamp = NewType("year", Union[int, float]) + + def annotated(a: Tuple[int, float], b: str, c: E) -> timestamp: + pass + + match((1, 2), Tuple[int, int], lambda a, b: (a, b)) # => (1, 2) + match(1, Union[str, int], lambda x: x) # => 1 + match('a', Union[str, int], lambda x: x) # => 'a' + match('a', Optional[str], lambda x: x) # => 'a' + match(None, Optional[str], lambda x: x) # => None + match(Pet, Type[Pet], lambda x: x) # => Pet + match(Cat, Type[Pet], lambda x: x) # => Cat + match(Dog, Any, lambda x: x) # => Dog + match(Dog, Type[Any], lambda x: x) # => Dog + match(15, timestamp, lambda x: x) # => 15 + match(10.0, timestamp, lambda x: x) # => 10.0 + match([1, 2, 3], List[int], lambda x: x) # => [1, 2, 3] + match({'a': 1, 'b': 2}, Dict[str, int], lambda x: x) # => {'a': 1, 'b': 2} + match(annotated, + Callable[[Tuple[int, float], str, Pet], timestamp], lambda x: x + ) # => annotated + ``` + For iterable generics actual type of value is guessed based on the first element. + ```python + match([1, 2, 3], List[int], lambda x: x) # => [1, 2, 3] + match([1, "b", "a"], List[int], lambda x: x) # => [1, "b", "a"] + match(["a", "b", "c"], List[int], lambda x: x) # raises MatchError + match(["a", "b", "c"], List[Union[str, int]], lambda x: x) # ["a", "b", "c"] + + match({"a": 1, "b": 2}, Dict[str, int], lambda x: x) # {"a": 1, "b": 2} + match({"a": 1, "b": "dog"}, Dict[str, int], lambda x: x) # {"a": 1, "b": "dog"} + match({"a": 1, 1: 2}, Dict[str, int], lambda x: x) # {"a": 1, 1: 2} + match({2: 1, 1: 2}, Dict[str, int], lambda x: x) # raises MatchError + match({2: 1, 1: 2}, Dict[Union[str, int], int], lambda x: x) # {2: 1, 1: 2} + ``` + Iterable generics also match with any of their subtypes. + ```python + match([1, 2, 3], Iterable[int], lambda x: x) # => [1, 2, 3] + match({1, 2, 3}, Iterable[int], lambda x: x) # => {1, 2, 3} + match(range(10), Iterable[int], lambda x: x) # => range(10) + + match([1, 2, 3], List[int], lambda x: x) # => [1, 2, 3] + match({1, 2, 3}, List[int], lambda x: x) # => raises MatchError + match(range(10), List[int], lambda x: x) # => raises MatchError + + match([1, 2, 3], Set[int], lambda x: x) # => raises MatchError + match({1, 2, 3}, Set[int], lambda x: x) # => {1, 2, 3} + match(range(10), Set[int], lambda x: x) # => raises MatchError + ``` + For Callable any arg without annotation treated as Any. + ```python + def annotated(a: int, b: int) -> float: + pass + + def not_annotated(a, b): + pass + + def partially_annotated(a, b: float): + pass + + match(annotated, Callable[[int, int], float], lambda x: x) # => annotated + match(not_annotated, Callable[[int, int], float], lambda x: x) # => raises MatchError + match(not_annotated, Callable[[Any, Any], Any], lambda x: x) # => not_annotated + match(annotated, Callable[[Any, Any], Any], lambda x: x) # => raises MatchError + match(partially_annotated, + Callable[[Any, float], Any], lambda x: x + ) # => partially_annotated + ``` + TypeVar is not supported. + ## All the things you can match As Pattern you can use any Python type, any class, or any Python value. @@ -191,6 +271,15 @@ | `{'type':'dog', age: int }` | Any dict with `type: "dog"` and with an `int` age | `{"type":"dog", "age": 3}` | `3` | `{"type":"dog", "age":2.3}` | | `re.compile('(\w+)-(\w+)-cat$')` | Any string that matches that regular expression expr | `"my-fuffy-cat"` | `"my"` and `"puffy"` | `"fuffy-dog"` | | `Pet(name=_, age=7)` | Any Pet dataclass with `age == 7` | `Pet('rover', 7)` | `['rover']` | `Pet('rover', 8)` | + | `Any` | The same as `_` | | that value | | + | `Union[int, float, None]` | Any integer or float number or None | `2.35` | `2.35` | any other value | + | `Optional[int]` | The same as `Union[int, None]` | `2` | `2` | any other value | + | `Type[MyClass]` | Any subclass of MyClass. **And any class that extends MyClass.** | `MyClass` | that class | any other object | + | `Callable[[int], float]` | Any callable with exactly that signature | `def a(q:int) -> float: ...` | that function | `def a(q) -> float: ...` | + | `Tuple[MyClass, int, float]` | The same as `(MyClass, int, float)` | | | | + | `Mapping[str, int]` Any subtype of `Mapping` acceptable too | any mapping or subtype of mapping with string keys and integer values | `{'a': 2, 'b': 3}` | that dict | `{'a': 'b', 'b': 'c'}` | + | `Iterable[int]` Any subtype of `Iterable` acceptable too | any iterable or subtype of iterable with integer values | `range(10)` and `[1, 2, 3]` | that iterable | `['a', 'b', 'v']` | + ## Using default @@ -234,7 +323,7 @@ or ```$ pip3 install pampy``` - ## If you really must to use Python2 + ## If you really must use Python2 Pampy is Python3-first, but you can use most of its features in Python2 via [this backport](https://pypi.org/project/backports.pampy/) by Manuel Barkhau: ```pip install backports.pampy``` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pampy-0.2.1/pampy.egg-info/SOURCES.txt new/pampy-0.3.0/pampy.egg-info/SOURCES.txt --- old/pampy-0.2.1/pampy.egg-info/SOURCES.txt 2018-12-24 15:08:24.000000000 +0100 +++ new/pampy-0.3.0/pampy.egg-info/SOURCES.txt 2019-11-07 16:50:51.000000000 +0100 @@ -12,4 +12,5 @@ tests/test_dataclass.py tests/test_dict.py tests/test_elaborate.py -tests/test_iterable.py \ No newline at end of file +tests/test_iterable.py +tests/test_typing.py \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pampy-0.2.1/tests/test_basic.py new/pampy-0.3.0/tests/test_basic.py --- old/pampy-0.2.1/tests/test_basic.py 2018-12-24 12:06:20.000000000 +0100 +++ new/pampy-0.3.0/tests/test_basic.py 2019-07-13 13:23:08.000000000 +0200 @@ -1,5 +1,5 @@ import unittest - +from enum import Enum import re from pampy import match_value, match, HEAD, TAIL, _, MatchError @@ -143,3 +143,12 @@ self.assertEqual(what_is('my-fuffy-cat'), 'fuffy-cat') + def test_match_enum(self): + class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + self.assertEqual(match(Color.RED, Color.BLUE, "blue", Color.RED, "red", _, "else"), "red") + self.assertEqual(match(Color.RED, Color.BLUE, "blue", Color.GREEN, "green", _, "else"), "else") + self.assertEqual(match(1, Color.BLUE, "blue", Color.GREEN, "green", _, "else"), "else") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pampy-0.2.1/tests/test_elaborate.py new/pampy-0.3.0/tests/test_elaborate.py --- old/pampy-0.2.1/tests/test_elaborate.py 2018-12-24 12:16:30.000000000 +0100 +++ new/pampy-0.3.0/tests/test_elaborate.py 2019-07-13 13:23:08.000000000 +0200 @@ -1,4 +1,12 @@ import unittest +from datetime import datetime +from typing import ( + Union, + Optional, + Tuple, + List, + NewType, +) from functools import reduce @@ -116,3 +124,107 @@ return sum(cutenesses) / len(cutenesses) self.assertEqual(avg_cuteness_pampy(), (4 + 3 + 4.6 + 7) / 4) + + def test_advanced_lambda(self): + def either(pattern1, pattern2): + """Matches values satisfying pattern1 OR pattern2""" + def repack(*args): + return True, list(args) + + def f(var): + return match(var, + pattern1, repack, + pattern2, repack, + _, (False, []) + ) + + return f + + self.assertEqual(match('str', either(int, str), 'success'), 'success') + + def datetime_p(year: int, month: int, day: int, hour: int = 0, minute: int = 0, second: int = 0): + """Matches a datetime with these values""" + def f(var: datetime): + if not isinstance(var, datetime): + return False, [] + + args = [] + for pattern, actual in [(year, var.year), (month, var.month), (day, var.day), + (hour, var.hour), (minute, var.minute), (second, var.second)]: + if pattern is _: + args.append(actual) + elif pattern != actual: + return False, [] + + return True, args + + return f + + def test(var): + return match(var, + datetime_p(2018, 12, 23), 'full match', + datetime_p(2018, _, _), lambda month, day: f'{month}/{day} in 2018', + datetime_p(_, _, _, _, _, _), 'any datetime', + _, 'not a datetime' + ) + + self.assertEqual(test(datetime(2018, 12, 23)), 'full match') + self.assertEqual(test(datetime(2018, 1, 2)), '1/2 in 2018') + self.assertEqual(test(datetime(2017, 1, 2, 3, 4, 5)), 'any datetime') + self.assertEqual(test(11), 'not a datetime') + + def test_typing_example(self): + + timestamp = NewType("timestamp", Union[float, int]) + year, month, day, hour, minute, second = int, int, int, int, int, int + day_tuple = Tuple[year, month, day] + dt_tuple = Tuple[year, month, day, hour, minute, second] + + def datetime_p(patterns: List[str]): + def f(dt: str): + for pattern in patterns: + try: + return True, [datetime.strptime(dt, pattern)] + except Exception: + continue + else: + return False, [] + return f + + def to_datetime(dt: Union[ + timestamp, + day_tuple, + dt_tuple, + str, + ]) -> Optional[datetime]: + return match(dt, + timestamp, lambda x: datetime.fromtimestamp(x), + Union[day_tuple, dt_tuple], lambda *x: datetime(*x), + datetime_p(["%Y-%m-%d", "%Y-%m-%d %H:%M:%S"]), lambda x: x, + _, None + ) + + key_date_tuple = (2018, 1, 1) + detailed_key_date_tuple = (2018, 1, 1, 12, 5, 6) + key_date = datetime(*key_date_tuple) + detailed_key_date = datetime(*detailed_key_date_tuple) + + self.assertEqual(to_datetime(key_date_tuple), key_date) + self.assertEqual(to_datetime(detailed_key_date_tuple), detailed_key_date) + + key_date_ts = key_date.timestamp() + detailed_key_date_ts = int(detailed_key_date.timestamp()) + self.assertEqual(to_datetime(key_date_ts), key_date) + self.assertEqual(to_datetime(detailed_key_date_ts), detailed_key_date) + + key_date_ts_str_a = key_date.strftime("%Y-%m-%d") + key_date_ts_str_f = key_date.strftime("%Y-%m-%d %H:%M:%S") + key_date_ts_str_w = key_date.strftime("%m-%Y-%d") + self.assertEqual(to_datetime(key_date_ts_str_a), key_date) + self.assertEqual(to_datetime(key_date_ts_str_f), key_date) + self.assertEqual(to_datetime(key_date_ts_str_w), None) + + detailed_key_date_ts_str = detailed_key_date.strftime("%Y-%m-%d %H:%M:%S") + self.assertEqual(to_datetime(detailed_key_date_ts_str), detailed_key_date) + + self.assertEqual(to_datetime(set(key_date_tuple)), None) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pampy-0.2.1/tests/test_typing.py new/pampy-0.3.0/tests/test_typing.py --- old/pampy-0.2.1/tests/test_typing.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pampy-0.3.0/tests/test_typing.py 2019-07-13 13:23:08.000000000 +0200 @@ -0,0 +1,211 @@ +import unittest +from typing import ( + Callable, + List, + Set, + FrozenSet, + Mapping, + Iterable, + Dict, + Any, + Union, + Tuple, + NewType, + Optional, + Type, +) + +from pampy import ( + match, + match_value, +) + + +class A: + pass + + +class B(A): + pass + + +class C(A): + pass + + +class Z: + pass + +E = NewType("E", B) +lol = NewType("lol", int) +kek = NewType("kek", lol) +cat = NewType("cat", str) +double_kek = NewType('double_kek', Tuple[kek, kek]) +composite_kek = NewType("CKek", Union[cat, double_kek]) + + +def annotated(a: int) -> float: + pass + + +def big_annotated(a: Tuple[int, float], b: str, c: E) -> double_kek: + pass + + +def wrong_annotated(b: str) -> str: + pass + + +def not_annotated(q): + pass + + +class PampyTypingTests(unittest.TestCase): + + def test_match_any(self): + self.assertEqual(match_value(Any, 3), (True, [3])) + self.assertEqual(match_value(Any, 'ok'), (True, ['ok'])) + + def test_match_union(self): + self.assertEqual(match_value(Union[int, str], 3), (True, [3])) + self.assertEqual(match_value(Union[int, str], 'ok'), (True, ['ok'])) + self.assertEqual(match_value(Union[int, str, float], 5.25), (True, [5.25])) + self.assertEqual(match_value(Optional[int], None), (True, [None])) + self.assertEqual(match_value(Optional[int], 1), (True, [1])) + self.assertEqual(match_value(Optional[int], 1.0), (False, [])) + + def test_match_newtype(self): + self.assertEqual(match_value(lol, 3), (True, [3])) + self.assertEqual(match_value(kek, 3), (True, [3])) + self.assertEqual(match_value(double_kek, (13, 37)), (True, [13, 37])) + self.assertEqual(match_value(composite_kek, "Barsik"), (True, ["Barsik"])) + self.assertEqual(match_value(composite_kek, (13, 37)), (True, [13, 37])) + + def test_match_type(self): + self.assertEqual(match_value(Type[int], int), (True, [int])) + self.assertEqual(match_value(Type[int], 1), (False, [])) + self.assertEqual(match_value(Type[E], "cat"), (False, [])) + self.assertEqual(match_value(Type[A], "cat"), (False, [])) + + self.assertEqual(match_value(Type[lol], int), (True, [int])) + self.assertEqual(match_value(Type[kek], int), (True, [int])) + self.assertEqual(match_value(Type[kek], str), (False, [])) + + self.assertEqual(match_value(Type[A], A), (True, [A])) + self.assertEqual(match_value(Type[A], B), (True, [B])) + self.assertEqual(match_value(Type[B], C), (False, [])) + self.assertEqual(match_value(Type[E], B), (True, [B])) + + self.assertEqual(match_value(Type[A], E), (True, [E])) + + self.assertEqual(match_value(Type[E], C), (False, [])) + self.assertEqual(match_value(Type[C], A), (False, [])) + + self.assertEqual(match_value(Type[Any], A), (True, [A])) + self.assertEqual(match_value(Type[Any], Z), (True, [Z])) + self.assertEqual(match_value(Type[Any], int), (True, [int])) + + def test_match_callable(self): + self.assertEqual(match_value(Callable[[int], float], annotated), (True, [annotated])) + self.assertEqual( + match_value(Callable[[Tuple[int, float], str, E], double_kek], big_annotated), (True, [big_annotated]) + ) + self.assertEqual(match_value(Callable[[int], float], not_annotated), (False, [])) + self.assertEqual(match_value(Callable[[Any], Any], not_annotated), (True, [not_annotated])) + self.assertEqual(match_value(Callable[[int], float], wrong_annotated), (False, [])) + + def test_match_tuple(self): + self.assertEqual( + match_value(Tuple[int, str], (1, "a")), + (True, [1, "a"]) + ) + self.assertEqual( + match_value(Tuple[int, str], (1, 1)), + (False, []) + ) + self.assertEqual( + match_value( + Tuple[ + Union[int, float], + Callable[[int], float], + Tuple[str, Type[E]] + ], + (1.0, annotated, ("ololo", B)) + ), + (True, [1.0, annotated, "ololo", B]) + ) + self.assertEqual( + match_value( + Tuple[ + Union[int, float], + Callable[[int], float], + Tuple[str, Type[E]] + ], + (1.0, False, ("ololo", B)) + ), + (False, []) + ) + self.assertEqual( + match_value( + Tuple[ + Union[int, float], + Callable[[int], float], + Tuple[str, Type[E]] + ], + ("kek", annotated, ("ololo", B)) + ), + (False, []) + ) + self.assertEqual( + match_value( + Tuple[ + Union[int, float], + Callable[[int], float], + Tuple[str, Type[E]] + ], + (1, annotated, ("ololo", B, 1488)) + ), + (False, []) + ) + self.assertEqual( + match_value( + Tuple[ + Union[int, float], + Callable[[int], float], + Tuple[str, Type[A]] + ], + (1, annotated, ("ololo", E)) + ), + (True, [1, annotated, "ololo", E]) + ) + + def test_match_mapping(self): + self.assertEqual(match_value(Dict[str, int], {"a": 1, "b": 2}), (True, [{"a": 1, "b": 2}])) + self.assertEqual(match_value(Mapping[str, lol], {"a": 1, "b": 2}), (True, [{"a": 1, "b": 2}])) + self.assertEqual(match_value(Dict[str, lol], {"a": 1, "b": 2}), (True, [{"a": 1, "b": 2}])) + self.assertEqual(match_value(Dict[str, int], {"a": 1.0, "b": 2.0}), (False, [])) + self.assertEqual(match_value(Dict[str, int], {1: 1, 2: 2}), (False, [])) + self.assertEqual(match_value(Mapping[str, int], {1: 1, 2: 2}), (False, [])) + self.assertEqual(match_value(Dict[Union[str, int], int], {1: 1, 2: 2}), (True, [{1: 1, 2: 2}])) + self.assertEqual(match_value(Dict[Union[str, lol], int], {1: 1, 2: 2}), (True, [{1: 1, 2: 2}])) + self.assertEqual( + match_value(Dict[str, Callable[[int], float]], {"a": annotated, "b": annotated}), + (True, [{"a": annotated, "b": annotated}]) + ) + + def test_match_iterable(self): + self.assertEqual(match_value(List[int], [1, 2, 3]), (True, [[1, 2, 3]])) + self.assertEqual(match_value(List[int], range(10)), (False, [])) + self.assertEqual(match_value(Iterable[int], [1, 2, 3]), (True, [[1, 2, 3]])) + self.assertEqual(match_value(Iterable[int], range(10)), (True, [range(10)])) + self.assertEqual(match_value(List[lol], [1, 2, 3]), (True, [[1, 2, 3]])) + self.assertEqual(match_value(List[str], [1, 2, 3]), (False, [])) + self.assertEqual(match_value(Iterable[str], [1, 2, 3]), (False, [])) + a_vals = [B(), C(), B()] + self.assertEqual(match_value(List[A], a_vals), (True, [a_vals])) + self.assertEqual(match_value(List[str], ["lol", "kek"]), (True, [["lol", "kek"]])) + self.assertEqual(match_value(List[Union[str, int]], ["lol", "kek"]), (True, [["lol", "kek"]])) + self.assertEqual(match_value(List[Union[str, int]], [1, 2, 3]), (True, [[1, 2, 3]])) + self.assertEqual(match_value(List[int], {1, 2, 3}), (False, [])) + self.assertEqual(match_value(Set[int], {1, 2, 3}), (True, [{1, 2, 3}])) + self.assertEqual(match_value(FrozenSet[int], frozenset([1, 2, 3])), (True, [frozenset([1, 2, 3])]))