diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | python-awesome-pattern-matching.spec | 2916 | ||||
-rw-r--r-- | sources | 1 |
3 files changed, 2918 insertions, 0 deletions
@@ -0,0 +1 @@ +/awesome-pattern-matching-0.24.4.tar.gz diff --git a/python-awesome-pattern-matching.spec b/python-awesome-pattern-matching.spec new file mode 100644 index 0000000..1589f13 --- /dev/null +++ b/python-awesome-pattern-matching.spec @@ -0,0 +1,2916 @@ +%global _empty_manifest_terminate_build 0 +Name: python-awesome-pattern-matching +Version: 0.24.4 +Release: 1 +Summary: Awesome Pattern Matching +License: MIT License +URL: https://github.com/scravy/awesome-pattern-matching +Source0: https://mirrors.nju.edu.cn/pypi/web/packages/e6/b5/aedee8533346618f2293632023f2eb62c106902534c2daddfc30531b2891/awesome-pattern-matching-0.24.4.tar.gz +BuildArch: noarch + + +%description +# Awesome Pattern Matching (_apm_) for Python + +[](https://github.com/scravy/awesome-pattern-matching/actions) +[](https://pepy.tech/project/awesome-pattern-matching) +[](https://pypi.org/project/awesome-pattern-matching/) + +```bash +pip install awesome-pattern-matching +``` + +- Simple +- Powerful +- Extensible +- Composable +- Functional +- Python 3.7+, PyPy3.7+ +- Typed (IDE friendly) +- Offers different styles (expression, declarative, statement, ...) + +There's a ton of pattern matching libraries available for python, all with varying degrees of maintenance and usability; +also [since Python 3.10 there is the PEP-634 `match` statement](https://www.python.org/dev/peps/pep-0634/). However, +this library still offers functionality that PEP-634 doesn't offer, as well as pattern matching for python versions +before 3.10. [A detailed comparison of PEP-634 and _`apm`_ is available](https://github.com/scravy/awesome-pattern-matching/blob/main/docs/apm_vs_pep634.md). + +_`apm`_ defines patterns as objects which are _composable_ and _reusable_. Pieces can be matched and captured into +variables, much like pattern matching in Haskell or Scala (a feature which most libraries actually lack, but which also +makes pattern matching useful in the first place - the capability to easily extract data). Here is an example: + +```python +from apm import * + +if result := match([1, 2, 3, 4, 5], [1, '2nd' @ _, '3rd' @ _, 'tail' @ Remaining(...)]): + print(result['2nd']) # 2 + print(result['3rd']) # 3 + print(result['tail']) # [4, 5] + +# If you find it more readable, '>>' can be used instead of '@' to capture a variable +match([1, 2, 3, 4, 5], [1, _ >> '2nd', _ >> '3rd', Remaining(...) >> 'tail']) +``` + +Patterns can be composed using `&`, `|`, and `^`, or via their more explicit counterparts `AllOf`, `OneOf`, and `Either` +. Since patterns are objects, they can be stored in variables and be reused. + +```python +positive_integer = InstanceOf(int) & Check(lambda x: x >= 0) +``` + +Some fancy matching patterns are available out of the box: + +```python +from apm import * + +def f(x: int, y: float) -> int: + pass + +if match(f, Arguments(int, float) & Returns(int)): + print("Function satisfies required signature") +``` + + + + +## Multiple Styles + +For matching and selecting from multiple cases, choose your style: + +```python +from apm import * + +value = 7 + +# The simple style +if match(value, Between(1, 10)): + print("It's between 1 and 10") +elif match(value, Between(11, 20)): + print("It's between 11 and 20") +else: + print("It's not between 1 and 20") + +# The expression style +case(value) \ + .of(Between(1, 10), lambda: print("It's between 1 and 10")) \ + .of(Between(11, 20), lambda: print("It's between 11 and 20")) \ + .otherwise(lambda: print("It's not between 1 and 20")) + +# The statement style +try: + match(value) +except Case(Between(1, 10)): + print("It's between 1 and 10") +except Case(Between(11, 20)): + print("It's between 11 and 20") +except Default: + print("It's not between 1 and 20") + +# The declarative style +@case_distinction +def f(n: Match(Between(1, 10))): + print("It's between 1 and 10") + +@case_distinction +def f(n: Match(Between(11, 20))): + print("It's between 11 and 20") + +@case_distinction +def f(n): + print("It's not between 1 and 20") + +f(value) + +# The terse (pampy) style +match(value, + Between( 1, 10), lambda: print("It's between 1 and 10"), + Between(11, 20), lambda: print("It's between 11 and 20"), + _, lambda: print("It's not between 1 and 20")) +``` + + +## Nested pattern matches + +Patterns are applied recursively, such that nested structures can be matched arbitrarily deep. +This is super useful for extracting data from complicated structures: + +```python +from apm import * + +sample_k8s_response = { + "containers": [ + { + "args": [ + "--cert-dir=/tmp", + "--secure-port=4443", + "--kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname", + "--kubelet-use-node-status-port" + ], + "image": "k8s.gcr.io/metrics-server/metrics-server:v0.4.1", + "imagePullPolicy": "IfNotPresent", + "name": "metrics-server", + "ports": [ + { + "containerPort": 4443, + "name": "https", + "protocol": "TCP" + } + ] + } + ] +} + +if result := match(sample_k8s_response, { + "containers": Each({ + "image": 'image' @ _, + "name": 'name' @ _, + "ports": Each({ + "containerPort": 'port' @ _ + }), + }) + }): + print(f"Image: {result['image']}, Name: {result['name']}, Port: {result['port']}") +``` + +The above will print + +``` +Image: k8s.gcr.io/metrics-server/metrics-server:v0.4.1, Name: metrics-server, Port: 4443 +``` + + +## Multimatch + +By default `match` records only the last match for captures. If for example `'item' @ InstanceOf(int)` matches multiple times, +the last match will be recorded in `result['item']`. `match` can record all captures using the `multimatch=True` flag: + +```python +if result := match([{'foo': 5}, 3, {'foo': 7, 'bar': 9}], Each(OneOf({'foo': 'item' @ _}, ...)), multimatch=True): + print(result['item']) # [5, 7] + +# The default since v0.15.0 is multimatch=False +if result := match([{'foo': 5}, 3, {'foo': 7, 'bar': 9}], Each(OneOf({'foo': 'item' @ _}, ...))): + print(result['item']) # 7 +``` + + +## Strict vs non-strict matches + +Any value which occurs verbatim in a pattern is matched verbatim (`int`, `str`, `list`, ...), except Dictionaries ( +anything which has an `items()` actually). + +Thus: + +```python +some_very_complex_object = { + "A": 1, + "B": 2, + "C": 3, +} +match(some_very_complex_object, {"C": 3}) # matches! +``` + +If you do not want unknown keys to be ignored, wrap the pattern in a `Strict`: + +```python +# does not match, only matches exactly `{"C": 3}` +match(some_very_complex_object, Strict({"C": 3})) +``` + +Lists (anything iterable which does not have an `items()` actually) are also compared as they are, i.e.: + +```python +ls = [1, 2, 3] +match(ls, [1, 2, 3]) # matches +match(ls, [1, 2]) # does not match +``` + + +## Match head and tail of a list + +It is possible to match the remainder of a list though: + +```python +match(ls, [1, 2, Remaining(InstanceOf(int))]) +``` + +And each item: + +```python +match(ls, Each(InstanceOf(int))) +``` + +Patterns can be joined using `&`, `|`, and `^`: + +```python +match(ls, Each(InstanceOf(int) & Between(1, 3))) +``` + +Wild-card matches are supported using Ellipsis (`...`): + +```python +match(ls, [1, Remaining(..., at_least=2)]) +``` + +The above example also showcases how `Remaining` can be made to match +`at_least` _n_ number of items (`Each` also has an `at_least` keyword argument). + + +## Wildcard matches anything using `_` + +A wildcard pattern can be expressed using `_`. `_` is a `Pattern` and thus `>>` and `@` can be used with it. + +```python +match([1, 2, 3, 4], [1, _, 3, _]) +``` + + +## Wildcard matches anything using `...` + +The `Ellipsis` can be used as a wildcard match, too. It is however not a `Pattern` (so `|`, `&`, `@`, etc. can not +be used on it). If you actually want to match `Ellipsis`, wrap it using `Value(...)`. + +Otherwise `...` is equivalent for most intents and purposes to `_`: + +```python +match([1, 2, 3, 4], [1, ..., 3, ...]) +``` + + +## Support for dataclasses + +```python +@dataclass +class User: + first_name: str + last_name: str + +value = User("Jane", "Doe") + +if match(value, User(_, "Doe")): + print("Welcome, member of the Doe family!") +elif match(value, User(_, _)): + print("Welcome, anyone!") +``` + + +## The different styles in detail + +### Simple style + +- 💚 has access to result captures +- 💚 vanilla python +- 💔 no case guards +- 💔 can not return values (since it's a statement, not an expression) +- 🖤 a bit repetetive +- 💚 simplest and most easy to understand style +- 🖤 fastest of them all + +```python +from apm import * + +value = {"a": 7, "b": "foo", "c": "bar"} + +if result := match(value, EachItem(_, 'value' @ InstanceOf(str) | ...), multimatch=True): + print(result['value']) # ["foo", "bar"] +``` + +#### pre `:=` version (Python 3.7) + +`bind()` can be used on a `MatchResult` to bind the matched items to an existing dictionary. + +```python +from apm import * + +value = {"a": 7, "b": "foo", "c": "bar"} + +result = {} +if match(value, EachItem(_, 'value' @ InstanceOf(str) | ...)).bind(result): + print(result['value']) # ["foo", "bar"] +elif match(value, {"quux": _ >> 'quux'}).bind(result): + print(result['quux']) +``` + +### Expression style + +- 💚 has access to result captures +- 💚 vanilla python +- 💚 can return values directly as it is an expression +- 💚 can use case guards via `when=` or `guarded` +- 🖤 so terse that it is sometimes hard to read + +The expression style is summarized: + +```python +case(value).of(pattern, action) ... .otherwise(default_action) +``` + +...where action is either a value or a callable. The captures from the matching result are bound to the named +parameters of the given callable, i.e. `result['foo']` and `result['bar']` from `'foo' @ _` and `'bar' @ _` will be +bound to `foo` and `bar` respectively in `lambda foo, bar: ...`. + +```python +from apm import * + +display_name = case({'user': 'some-user-id', 'first_name': "Jane", 'last_name': "Doe"}) \ + .of({'first_name': 'first' @ _, 'last_name': 'last' @ _}, lambda first, last: f"{first}, {last}") \ + .of({'user': 'user_id' @ _}, lambda user_id: f"#{user_id}") \ + .otherwise("anonymous") +``` + +_Note: To return a value an `.otherwise(...)` case must always be present._ + + +### Statement style + +This is arguable the most hacky style in _`apm`_, as it re-uses the `try .. except` +mechanism. It is nevertheless quite readable. + +- 💚 has access to result captures +- 💚 very readable +- 💔 can not return values (since it's a statement, not an expression) +- 💚 can use case guards via `when=` +- 🖤 misuse of the `try .. except` statement + +```python +from apm import * + +try: + match({'user': 'some-user-id', 'first_name': "Jane", 'last_name': "Doe"}) +except Case({'first_name': 'first' @ _, 'last_name': 'last' @ _}) as result: + user = f"{result['first']} {result['last']}" +except Case({'user': 'user_id' @ _}) as result: + user = f"#{result['user_id']}" +except Default: + user = "anonymous" + +print(user) # "Jane Doe" +``` + + +### Declarative style + +- 💔 does not have access to result captures +- 💚 very readable +- 💚 can use case guards via `when=` +- 💚 can return values +- 🖤 the most bloated version of all styles + +```python +from apm import * + +@case_distinction +def fib(n: Match(OneOf(0, 1))): + return n + +@case_distinction +def fib(n): + return fib(n - 2) + fib(n - 1) + +for i in range(0, 6): + print(fib(i)) +``` + +#### Nota bene: Overloading using `@case_distinction` + +If not for its pattern matching capabilities, `@case_distinction` can be used +to implement overloading. In fact, it can be imported as `@overload`. +The mechanism is aware of arity and argument types. + +```python +from apm.overload import overload + +@overload +def add(a: str, b: str): + return "".join([a, b]) + +@overload +def add(a: int, b: int): + return a + b + +add("a", "b") +add(1, 2) +``` + +### Terse style + +- 💚 has access to result captures +- 💚 can use case guards via `guarded` +- 💚 very concise +- 💚 can return values +- 🖤 very readable when formatted nicely +- 🖤 not so well suited for larger match actions +- 🖤 slowest of them all + +As the name indicates the "terse" style is terse. It is inspired by the `pampy` +pattern matching library and mimics some of its behavior. Despite a slim surface +area it also comes with some simplifications: + +- A type given as a pattern is matched against as if it was wrapped in an `InstanceOf` +- `re.Pattern` objects (result of `re.compile`) are matched against as if it was given via `Regex` +- Captures are passed to actions in the same order as they occur in the pattern (not by name) + +```python +from apm import * + +def fibonacci(n): + return match(n, + 1, 1, + 2, 1, + _, lambda x: fibonacci(x - 1) + fibonacci(x - 2) + ) + +fibonacci(6) # -> 8 + + +class Animal: pass +class Hippo(Animal): pass +class Zebra(Animal): pass +class Horse(Animal): pass + +def what_am_i(x): + return match(x, + Hippo, 'hippopotamus', + Zebra, 'zebra', + Animal, 'some other animal', + _, 'not at all an animal', + ) + +what_am_i(Hippo()) # -> 'hippopotamus' +what_am_i(Zebra()) # -> 'zebra' +what_am_i(Horse()) # -> 'some other animal' +what_am_i(42) # -> 'not at all an animal' +``` + + +## Available patterns + +### `Capture(pattern, name=<str>)` + +Captures a piece of the thing being matched by name. + +```python +if result := match([1, 2, 3, 4], [1, 2, Capture(Remaining(InstanceOf(int)), name='tail')]): + print(result['tail']) ## -> [3, 4] +``` + +As this syntax is rather verbose, two shorthand notations can be used: + +```python +# using the matrix multiplication operator '@' (syntax resembles that of Haskell and Scala) +if result := match([1, 2, 3, 4], [1, 2, 'tail' @ Remaining(InstanceOf(int))]): + print(result['tail']) ## -> [3, 4] + +# using the right shift operator +if result := match([1, 2, 3, 4], [1, 2, Remaining(InstanceOf(int)) >> 'tail']): + print(result['tail']) ## -> [3, 4] +``` + + +### `Strict(pattern)` + +Performs a strict pattern match. A strict pattern match also compares the type of verbatim values. That is, while +_`apm`_ would match `3` with `3.0` it would not do so when using `Strict`. Also _`apm`_ performs partial matches of +dictionaries (that is: it ignores unknown keys). It will perform an exact match for dictionaries using `Strict`. + +```python +# The following will match +match({"a": 3, "b": 7}, {"a": ...}) +match(3.0, 3) + +# These will not match +match({"a": 3, "b": 7}, Strict({"a": ...})) +match(3.0, Strict(3)) +``` + + +### `OneOf(*pattern)` + +Matches against any of the provided patterns. Equivalent to `p1 | p2 | p3 | ..` +(but operator overloading does not work with values that do not inherit from `Pattern`) + +```python +match("quux", OneOf("bar", "baz", "quux")) +``` + +```python +match(3, OneOf(InstanceOf(int), None)) +``` + +Patterns can also be joined using `|` to form a `OneOf` pattern: + +```python +match(3, InstanceOf(int) | InstanceOf(float)) +``` + +The above example is rather contrived, as `InstanceOf` already accepts multiple types natively: + +```python +match(3, InstanceOf(int, float)) +``` + +Since bare values do not inherit from `Pattern` they can be wrapped in `Value`: + +```python +match("quux", Value("foo") | Value("quux")) +``` + + +### `AllOf(*pattern)` + +Checks whether the value matches all of the given pattern. Equivalent to `p1 & p2 & p3 & ..` +(but operator overloading does not work with values that do not inherit from `Pattern`) + +```python +match("quux", AllOf(InstanceOf("str"), Regex("[a-z]+"))) +``` + + +### `NoneOf(*pattern)` + +Same as `Not(OneOf(*pattern))` (also `~OneOf(*pattern)`). + + +### `Not(pattern)` + +Matches if the given pattern does not match. + +```python +match(3, Not(4)) # matches +match(5, Not(4)) # matches +match(4, Not(4)) # does not match +``` + +The bitflip prefix operator (`~`) can be used to express the same thing. Note that it does not work on bare values, +so they need to be wrapped in `Value`. + +```python +match(3, ~Value(4)) # matches +match(5, ~Value(4)) # matches +match(4, ~Value(4)) # does not match +``` + +`Not` can be used do create a `NoneOf` kind of pattern: + +```python +match("string", ~OneOf("foo", "bar")) # matches everything except "foo" and "bar" +``` + +`Not` can be used to create a pattern that never matches: + +```python +Not(...) +``` + + +### `Each(pattern [, at_least=]` + +Matches each item in an iterable. + +```python +match(range(1, 10), Each(Between(1, 9))) +``` + + +### `EachItem(key_pattern, value_pattern)` + +Matches an object if each key satisfies `key_pattern` and each value satisfies `value_pattern`. + +```python +match({"a": 1, "b": 2}, EachItem(Regex("[a-z]+"), InstanceOf(int))) +``` + + +### `Some(pattern)` (aka `Many` and `Remaining`) + +Matches a sequence of items within a list: + +```python +if result := match(range(1, 10), [1, 'a' @ Some(...), 4, 'b' @ Some(...), 8, 9]): + print(result['a']) # [2, 3] + print(result['b']) # [5, 6, 7] +``` + +Takes the optional values `exactly`, `at_least`, and `at_most` which makes `Some` match +either `exactly` _n_ items, `at_least` _n_, or `at_most` _n_ items (`at_least` and `at_most` can be given at the same +time, but not together with `exactly`). + +Note the difference between `Some(1, 2)` and `Some([1, 2])`. The first version matches subsequences, the second +version matches items which are themselves lists: + +```python +match([0, 1, 2 , 1, 2 , 3], [0, Some( 1, 2 ), 3]) # matches the subsequence 1, 2 twice +match([0, [1, 2], [1, 2], 3], [0, Some([1, 2]), 3]) # matches the item [1, 2] twice, which happen to be lists +``` + +`Some` also goes by the names of `Many` and `Remaining`, which is sometimes nice to convey meaning: + +```python +match(range(1, 10), [1, 2, 'remaining' @ Remaining()]) +match([0, 1, 1, 1, 2, 1], [0, Many(1), Remaining(InstanceOf(int))]) +``` + +When used with no arguments, `Some()` is the same as `Some(...)`. + + +### `Remainder(pattern)` + +Can be used to match the unmatched parts of a Dictionary/Mapping. + +```python +result = match({ + "foo": 1, + "bar": 2, + "qux": 4, + "quuz": 8, +}, {"foo": 'foo' @ _, "bar": 'bar' @ _} ** Remainder('rs' @ _)) +print(result.foo) # 1 +print(result.bar) # 2 +print(result.rs) # {'qux': 4, 'quuz': 8} +``` + +`Remainder` is, strictly speaking, not a `Pattern` and only works in conjunction with `**` on dictionaries, +and it only works on the right-hand side of the dictionary. + + +### `Between(lower, upper)` + +Matches an object if it is between `lower` and `upper` (inclusive). The optional keyword arguments +`lower_bound_exclusive` and `upper_bound_exclusive` can be set to `True` respectively to exclude the +lower/upper from the range of matching values. + + +### `Length(length)` + +Matches an object if it has the given length. Alternatively also accepts `at_least` and `at_most` keyword arguments. + +```python +match("abc", Length(3)) +match("abc", Length(at_least=2)) +match("abc", Length(at_most=4)) +match("abc", Length(at_least=2, at_most=4)) +``` + + +### `Contains(item)` + +Matches an object if it contains the given item (as per the same logic as the `in` operator). + +```python +match("hello there, world", Contains("there")) +match([1, 2, 3], Contains(2) & Contains(3)) +match({'foo': 1, 'bar': 2}, Contains('quux') | Contains('bar')) +``` + + +### `Regex(regex_pattern, bind_groups: bool = True)` + +Matches a string if it completely matches the given regex, as per `re.fullmatch`. +If the regular expression pattern contains named capturing groups and `bind_groups` is set to `True`, +this pattern will bind the captured results in the `MatchResult` (the default). + +To mimic `re.match` or `re.search` the given regular expression `x` can be augmented as `x.*` or `.*x.*` +respectively. + + +### `Check(predicate)` + +Matches an object if it satisfies the given predicate. + +```python +match(2, Check(lambda x: x % 2 == 0)) +``` + + +### `InstanceOf(*types)` + +Matches an object if it is an instance of any of the given types. + +```python +match(1, InstanceOf(int, flaot)) +``` + + +### `SubclassOf(*types)` + +Matches if the matched type is a subclass of any of the given types. + +```python +match(int, SubclassOf(int, float)) +``` + + +### `Parameters(...)` + +Matches the parameters of a callable. + +```python +def f(x: int, *xs: float, y: str, **kwargs: bool): + pass + + +match(f, Parameters(int, VarArgs(float), y=str, KwArgs(bool))) +``` + +Each argument to Parameters is expected to be the type of a positional argument. + +`Parameters` matches function signatures if their positional arguments match completely, i.e. + +```python +def f(x: int, y: float): + pass + + +print(bool(match(f, Parameters(int)))) # False +print(bool(match(f, Parameters(int, float)))) # True +print(bool(match(f, Parameters(int, Remaining(_))))) # True +``` + +Keyword arguments are matched only if they are keyword only arguments. In contrast to positional arguments it matches +also impartially (which aligns with the non-strict matching behavior with respect to dictionaries): + +```python +def f(x: int, *, y: str, z: float): + pass + + +print(bool(match(f, Parameters(int)))) # True +print(bool(match(f, Parameters(y=str)))) # False – positional parameters not matched +print(bool(match(f, Parameters(int, y=str)))) # True +``` + +This can be changed with `Strict`: + +```python +def f(x: int, *, y: str, z: float): + pass + + +print(bool(match(f, Strict(Parameters(int))))) # False +print(bool(match(f, Strict(Parameters(int, y=str))))) # False (z not mentioned but present) +print(bool(match(f, Strict(Parameters(int, y=str, z=float))))) # True (has y and z exactly) +``` + + +### `Arguments(*types)` + +<span style="color: red">**DEPRECATED, use `Parameters` instead (see above)**</span> + + +Matches a callable if it's type annotations correspond to the given types. + +```python +def f(x: int, y: float, z): + ... + + +match(f, Arguments(int, float, None)) +``` + +Arguments has an alternate form which can be used to match keyword arguments: + +```python + +def f(x: int, y: float, z: str): + ... + +match(f, Arguments(x=int, y=float)) +``` + +The strictness rules are the same as for dictionaries (which is why the above example works). + +```python +# given the f from above +match(f, Strict(Arguments(x=int, y=float))) # does not match +match(f, Strict(Arguments(x=int, y=float, z=str))) # matches +``` + + +### `Returns(type)` + +Matches a callable if it's type annotations denote the given return type. + +```python +def g(x: int) -> str: + ... + + +match(g, Arguments(int) & Returns(str)) +``` + + +### `Transformed(function, pattern)` + +Transforms the currently looked at value by applying `function` on it and matches the result against `pattern`. In +Haskell and other languages this is known as a [_view +pattern_](https://gitlab.haskell.org/ghc/ghc/-/wikis/view-patterns). + +```python +def sha256(v: str) -> str: + import hashlib + return hashlib.new('sha256', v.encode('utf8')).hexdigest() + +match("hello", Transformed(sha256, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")) +``` + +This is handy for matching data types like `datetime.date` as this pattern won't match if the transformation +function errored out with an exception. + +```python +from apm import * +from datetime import date + +if result := match("2020-08-27", Transformed(date.fromisoformat, 'date' @ _): + print(repr(result['date'])) # result['date'] is a datetime.date +``` + + +### `At(path, pattern)` + +Checks whether the nested object to be matched satisfies pattern at the given path. The match fails if the given path +can not be resolved. + +```python +record = { + "foo": { + "bar": { + "quux": { + "value": "deeply nested" + } + } + } +} + +result := match(record, At("foo.bar.quux", {"value": Capture(..., name="value")})) +result['value'] # "deeply nested" + +# alternate form +result := match(record, At(['foo', 'bar', 'quux'], {"value": Capture(..., name="value")})) +``` + + +### `Items(**kwargs))` + +Mostly syntactic sugar to match a dictionary nicely (and anything that provides an `.items()` method). + +```python +from apm import * +from datetime import datetime + +request = { + "api_version": "v1", + "job": { + "run_at": "2020-08-27 14:09:30", + "command": "echo 'booya'", + } +} + +if result := match(request, Items( + api_version="v1", + job=Object( + run_at=Transformed(datetime.fromisoformat, 'time' @ _), + ) & OneOf( + Items(command='command' @ InstanceOf(str)), + Items(spawn='container' @ InstanceOf(str)), + ) +)): + print(repr(result['time'])) # datetime(2020, 8, 27, 14, 9, 30) + print('container' not in result) # True + print(result['command']) # "echo 'booya'" +``` + + +### `Object(type, *args, **kwargs)` + +Matches any object of the specific type with the given attrs as in `**kwargs`. +It respects the `__match_args__` introduced by PEP-634. + +```python +from apm import * +from typing import Literal, Tuple + +class Click: + __match_args__ = ("position", "button") + + def __init__(self, pos: Tuple[int, int], btn: Literal['left', 'right', 'middle']): + self.position = pos + self.button = btn + +assert match(Click((1, 2), 'left'), Object(Click, (1, 2))) +assert match(Click((1, 2), 'left'), Object(Click, (1, 2), 'left')) +assert match(Click((1, 2), 'left'), Object(Click, (1, 2), button='left')) +``` + + +## Extensible + +New patterns can be added, just like the ones in `apm.patterns.*`. Simply extend the `apm.Pattern` class: + +```python +class Min(Pattern): + def __init__(self, min): + self.min = min + + def match(self, value, *, ctx: MatchContext, strict=False) -> MatchResult: + return ctx.match_if(value >= self.min) + +match(3, Min(1)) # matches +match(3, Min(5)) # does not match +``` + + + + +%package -n python3-awesome-pattern-matching +Summary: Awesome Pattern Matching +Provides: python-awesome-pattern-matching +BuildRequires: python3-devel +BuildRequires: python3-setuptools +BuildRequires: python3-pip +%description -n python3-awesome-pattern-matching +# Awesome Pattern Matching (_apm_) for Python + +[](https://github.com/scravy/awesome-pattern-matching/actions) +[](https://pepy.tech/project/awesome-pattern-matching) +[](https://pypi.org/project/awesome-pattern-matching/) + +```bash +pip install awesome-pattern-matching +``` + +- Simple +- Powerful +- Extensible +- Composable +- Functional +- Python 3.7+, PyPy3.7+ +- Typed (IDE friendly) +- Offers different styles (expression, declarative, statement, ...) + +There's a ton of pattern matching libraries available for python, all with varying degrees of maintenance and usability; +also [since Python 3.10 there is the PEP-634 `match` statement](https://www.python.org/dev/peps/pep-0634/). However, +this library still offers functionality that PEP-634 doesn't offer, as well as pattern matching for python versions +before 3.10. [A detailed comparison of PEP-634 and _`apm`_ is available](https://github.com/scravy/awesome-pattern-matching/blob/main/docs/apm_vs_pep634.md). + +_`apm`_ defines patterns as objects which are _composable_ and _reusable_. Pieces can be matched and captured into +variables, much like pattern matching in Haskell or Scala (a feature which most libraries actually lack, but which also +makes pattern matching useful in the first place - the capability to easily extract data). Here is an example: + +```python +from apm import * + +if result := match([1, 2, 3, 4, 5], [1, '2nd' @ _, '3rd' @ _, 'tail' @ Remaining(...)]): + print(result['2nd']) # 2 + print(result['3rd']) # 3 + print(result['tail']) # [4, 5] + +# If you find it more readable, '>>' can be used instead of '@' to capture a variable +match([1, 2, 3, 4, 5], [1, _ >> '2nd', _ >> '3rd', Remaining(...) >> 'tail']) +``` + +Patterns can be composed using `&`, `|`, and `^`, or via their more explicit counterparts `AllOf`, `OneOf`, and `Either` +. Since patterns are objects, they can be stored in variables and be reused. + +```python +positive_integer = InstanceOf(int) & Check(lambda x: x >= 0) +``` + +Some fancy matching patterns are available out of the box: + +```python +from apm import * + +def f(x: int, y: float) -> int: + pass + +if match(f, Arguments(int, float) & Returns(int)): + print("Function satisfies required signature") +``` + + + + +## Multiple Styles + +For matching and selecting from multiple cases, choose your style: + +```python +from apm import * + +value = 7 + +# The simple style +if match(value, Between(1, 10)): + print("It's between 1 and 10") +elif match(value, Between(11, 20)): + print("It's between 11 and 20") +else: + print("It's not between 1 and 20") + +# The expression style +case(value) \ + .of(Between(1, 10), lambda: print("It's between 1 and 10")) \ + .of(Between(11, 20), lambda: print("It's between 11 and 20")) \ + .otherwise(lambda: print("It's not between 1 and 20")) + +# The statement style +try: + match(value) +except Case(Between(1, 10)): + print("It's between 1 and 10") +except Case(Between(11, 20)): + print("It's between 11 and 20") +except Default: + print("It's not between 1 and 20") + +# The declarative style +@case_distinction +def f(n: Match(Between(1, 10))): + print("It's between 1 and 10") + +@case_distinction +def f(n: Match(Between(11, 20))): + print("It's between 11 and 20") + +@case_distinction +def f(n): + print("It's not between 1 and 20") + +f(value) + +# The terse (pampy) style +match(value, + Between( 1, 10), lambda: print("It's between 1 and 10"), + Between(11, 20), lambda: print("It's between 11 and 20"), + _, lambda: print("It's not between 1 and 20")) +``` + + +## Nested pattern matches + +Patterns are applied recursively, such that nested structures can be matched arbitrarily deep. +This is super useful for extracting data from complicated structures: + +```python +from apm import * + +sample_k8s_response = { + "containers": [ + { + "args": [ + "--cert-dir=/tmp", + "--secure-port=4443", + "--kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname", + "--kubelet-use-node-status-port" + ], + "image": "k8s.gcr.io/metrics-server/metrics-server:v0.4.1", + "imagePullPolicy": "IfNotPresent", + "name": "metrics-server", + "ports": [ + { + "containerPort": 4443, + "name": "https", + "protocol": "TCP" + } + ] + } + ] +} + +if result := match(sample_k8s_response, { + "containers": Each({ + "image": 'image' @ _, + "name": 'name' @ _, + "ports": Each({ + "containerPort": 'port' @ _ + }), + }) + }): + print(f"Image: {result['image']}, Name: {result['name']}, Port: {result['port']}") +``` + +The above will print + +``` +Image: k8s.gcr.io/metrics-server/metrics-server:v0.4.1, Name: metrics-server, Port: 4443 +``` + + +## Multimatch + +By default `match` records only the last match for captures. If for example `'item' @ InstanceOf(int)` matches multiple times, +the last match will be recorded in `result['item']`. `match` can record all captures using the `multimatch=True` flag: + +```python +if result := match([{'foo': 5}, 3, {'foo': 7, 'bar': 9}], Each(OneOf({'foo': 'item' @ _}, ...)), multimatch=True): + print(result['item']) # [5, 7] + +# The default since v0.15.0 is multimatch=False +if result := match([{'foo': 5}, 3, {'foo': 7, 'bar': 9}], Each(OneOf({'foo': 'item' @ _}, ...))): + print(result['item']) # 7 +``` + + +## Strict vs non-strict matches + +Any value which occurs verbatim in a pattern is matched verbatim (`int`, `str`, `list`, ...), except Dictionaries ( +anything which has an `items()` actually). + +Thus: + +```python +some_very_complex_object = { + "A": 1, + "B": 2, + "C": 3, +} +match(some_very_complex_object, {"C": 3}) # matches! +``` + +If you do not want unknown keys to be ignored, wrap the pattern in a `Strict`: + +```python +# does not match, only matches exactly `{"C": 3}` +match(some_very_complex_object, Strict({"C": 3})) +``` + +Lists (anything iterable which does not have an `items()` actually) are also compared as they are, i.e.: + +```python +ls = [1, 2, 3] +match(ls, [1, 2, 3]) # matches +match(ls, [1, 2]) # does not match +``` + + +## Match head and tail of a list + +It is possible to match the remainder of a list though: + +```python +match(ls, [1, 2, Remaining(InstanceOf(int))]) +``` + +And each item: + +```python +match(ls, Each(InstanceOf(int))) +``` + +Patterns can be joined using `&`, `|`, and `^`: + +```python +match(ls, Each(InstanceOf(int) & Between(1, 3))) +``` + +Wild-card matches are supported using Ellipsis (`...`): + +```python +match(ls, [1, Remaining(..., at_least=2)]) +``` + +The above example also showcases how `Remaining` can be made to match +`at_least` _n_ number of items (`Each` also has an `at_least` keyword argument). + + +## Wildcard matches anything using `_` + +A wildcard pattern can be expressed using `_`. `_` is a `Pattern` and thus `>>` and `@` can be used with it. + +```python +match([1, 2, 3, 4], [1, _, 3, _]) +``` + + +## Wildcard matches anything using `...` + +The `Ellipsis` can be used as a wildcard match, too. It is however not a `Pattern` (so `|`, `&`, `@`, etc. can not +be used on it). If you actually want to match `Ellipsis`, wrap it using `Value(...)`. + +Otherwise `...` is equivalent for most intents and purposes to `_`: + +```python +match([1, 2, 3, 4], [1, ..., 3, ...]) +``` + + +## Support for dataclasses + +```python +@dataclass +class User: + first_name: str + last_name: str + +value = User("Jane", "Doe") + +if match(value, User(_, "Doe")): + print("Welcome, member of the Doe family!") +elif match(value, User(_, _)): + print("Welcome, anyone!") +``` + + +## The different styles in detail + +### Simple style + +- 💚 has access to result captures +- 💚 vanilla python +- 💔 no case guards +- 💔 can not return values (since it's a statement, not an expression) +- 🖤 a bit repetetive +- 💚 simplest and most easy to understand style +- 🖤 fastest of them all + +```python +from apm import * + +value = {"a": 7, "b": "foo", "c": "bar"} + +if result := match(value, EachItem(_, 'value' @ InstanceOf(str) | ...), multimatch=True): + print(result['value']) # ["foo", "bar"] +``` + +#### pre `:=` version (Python 3.7) + +`bind()` can be used on a `MatchResult` to bind the matched items to an existing dictionary. + +```python +from apm import * + +value = {"a": 7, "b": "foo", "c": "bar"} + +result = {} +if match(value, EachItem(_, 'value' @ InstanceOf(str) | ...)).bind(result): + print(result['value']) # ["foo", "bar"] +elif match(value, {"quux": _ >> 'quux'}).bind(result): + print(result['quux']) +``` + +### Expression style + +- 💚 has access to result captures +- 💚 vanilla python +- 💚 can return values directly as it is an expression +- 💚 can use case guards via `when=` or `guarded` +- 🖤 so terse that it is sometimes hard to read + +The expression style is summarized: + +```python +case(value).of(pattern, action) ... .otherwise(default_action) +``` + +...where action is either a value or a callable. The captures from the matching result are bound to the named +parameters of the given callable, i.e. `result['foo']` and `result['bar']` from `'foo' @ _` and `'bar' @ _` will be +bound to `foo` and `bar` respectively in `lambda foo, bar: ...`. + +```python +from apm import * + +display_name = case({'user': 'some-user-id', 'first_name': "Jane", 'last_name': "Doe"}) \ + .of({'first_name': 'first' @ _, 'last_name': 'last' @ _}, lambda first, last: f"{first}, {last}") \ + .of({'user': 'user_id' @ _}, lambda user_id: f"#{user_id}") \ + .otherwise("anonymous") +``` + +_Note: To return a value an `.otherwise(...)` case must always be present._ + + +### Statement style + +This is arguable the most hacky style in _`apm`_, as it re-uses the `try .. except` +mechanism. It is nevertheless quite readable. + +- 💚 has access to result captures +- 💚 very readable +- 💔 can not return values (since it's a statement, not an expression) +- 💚 can use case guards via `when=` +- 🖤 misuse of the `try .. except` statement + +```python +from apm import * + +try: + match({'user': 'some-user-id', 'first_name': "Jane", 'last_name': "Doe"}) +except Case({'first_name': 'first' @ _, 'last_name': 'last' @ _}) as result: + user = f"{result['first']} {result['last']}" +except Case({'user': 'user_id' @ _}) as result: + user = f"#{result['user_id']}" +except Default: + user = "anonymous" + +print(user) # "Jane Doe" +``` + + +### Declarative style + +- 💔 does not have access to result captures +- 💚 very readable +- 💚 can use case guards via `when=` +- 💚 can return values +- 🖤 the most bloated version of all styles + +```python +from apm import * + +@case_distinction +def fib(n: Match(OneOf(0, 1))): + return n + +@case_distinction +def fib(n): + return fib(n - 2) + fib(n - 1) + +for i in range(0, 6): + print(fib(i)) +``` + +#### Nota bene: Overloading using `@case_distinction` + +If not for its pattern matching capabilities, `@case_distinction` can be used +to implement overloading. In fact, it can be imported as `@overload`. +The mechanism is aware of arity and argument types. + +```python +from apm.overload import overload + +@overload +def add(a: str, b: str): + return "".join([a, b]) + +@overload +def add(a: int, b: int): + return a + b + +add("a", "b") +add(1, 2) +``` + +### Terse style + +- 💚 has access to result captures +- 💚 can use case guards via `guarded` +- 💚 very concise +- 💚 can return values +- 🖤 very readable when formatted nicely +- 🖤 not so well suited for larger match actions +- 🖤 slowest of them all + +As the name indicates the "terse" style is terse. It is inspired by the `pampy` +pattern matching library and mimics some of its behavior. Despite a slim surface +area it also comes with some simplifications: + +- A type given as a pattern is matched against as if it was wrapped in an `InstanceOf` +- `re.Pattern` objects (result of `re.compile`) are matched against as if it was given via `Regex` +- Captures are passed to actions in the same order as they occur in the pattern (not by name) + +```python +from apm import * + +def fibonacci(n): + return match(n, + 1, 1, + 2, 1, + _, lambda x: fibonacci(x - 1) + fibonacci(x - 2) + ) + +fibonacci(6) # -> 8 + + +class Animal: pass +class Hippo(Animal): pass +class Zebra(Animal): pass +class Horse(Animal): pass + +def what_am_i(x): + return match(x, + Hippo, 'hippopotamus', + Zebra, 'zebra', + Animal, 'some other animal', + _, 'not at all an animal', + ) + +what_am_i(Hippo()) # -> 'hippopotamus' +what_am_i(Zebra()) # -> 'zebra' +what_am_i(Horse()) # -> 'some other animal' +what_am_i(42) # -> 'not at all an animal' +``` + + +## Available patterns + +### `Capture(pattern, name=<str>)` + +Captures a piece of the thing being matched by name. + +```python +if result := match([1, 2, 3, 4], [1, 2, Capture(Remaining(InstanceOf(int)), name='tail')]): + print(result['tail']) ## -> [3, 4] +``` + +As this syntax is rather verbose, two shorthand notations can be used: + +```python +# using the matrix multiplication operator '@' (syntax resembles that of Haskell and Scala) +if result := match([1, 2, 3, 4], [1, 2, 'tail' @ Remaining(InstanceOf(int))]): + print(result['tail']) ## -> [3, 4] + +# using the right shift operator +if result := match([1, 2, 3, 4], [1, 2, Remaining(InstanceOf(int)) >> 'tail']): + print(result['tail']) ## -> [3, 4] +``` + + +### `Strict(pattern)` + +Performs a strict pattern match. A strict pattern match also compares the type of verbatim values. That is, while +_`apm`_ would match `3` with `3.0` it would not do so when using `Strict`. Also _`apm`_ performs partial matches of +dictionaries (that is: it ignores unknown keys). It will perform an exact match for dictionaries using `Strict`. + +```python +# The following will match +match({"a": 3, "b": 7}, {"a": ...}) +match(3.0, 3) + +# These will not match +match({"a": 3, "b": 7}, Strict({"a": ...})) +match(3.0, Strict(3)) +``` + + +### `OneOf(*pattern)` + +Matches against any of the provided patterns. Equivalent to `p1 | p2 | p3 | ..` +(but operator overloading does not work with values that do not inherit from `Pattern`) + +```python +match("quux", OneOf("bar", "baz", "quux")) +``` + +```python +match(3, OneOf(InstanceOf(int), None)) +``` + +Patterns can also be joined using `|` to form a `OneOf` pattern: + +```python +match(3, InstanceOf(int) | InstanceOf(float)) +``` + +The above example is rather contrived, as `InstanceOf` already accepts multiple types natively: + +```python +match(3, InstanceOf(int, float)) +``` + +Since bare values do not inherit from `Pattern` they can be wrapped in `Value`: + +```python +match("quux", Value("foo") | Value("quux")) +``` + + +### `AllOf(*pattern)` + +Checks whether the value matches all of the given pattern. Equivalent to `p1 & p2 & p3 & ..` +(but operator overloading does not work with values that do not inherit from `Pattern`) + +```python +match("quux", AllOf(InstanceOf("str"), Regex("[a-z]+"))) +``` + + +### `NoneOf(*pattern)` + +Same as `Not(OneOf(*pattern))` (also `~OneOf(*pattern)`). + + +### `Not(pattern)` + +Matches if the given pattern does not match. + +```python +match(3, Not(4)) # matches +match(5, Not(4)) # matches +match(4, Not(4)) # does not match +``` + +The bitflip prefix operator (`~`) can be used to express the same thing. Note that it does not work on bare values, +so they need to be wrapped in `Value`. + +```python +match(3, ~Value(4)) # matches +match(5, ~Value(4)) # matches +match(4, ~Value(4)) # does not match +``` + +`Not` can be used do create a `NoneOf` kind of pattern: + +```python +match("string", ~OneOf("foo", "bar")) # matches everything except "foo" and "bar" +``` + +`Not` can be used to create a pattern that never matches: + +```python +Not(...) +``` + + +### `Each(pattern [, at_least=]` + +Matches each item in an iterable. + +```python +match(range(1, 10), Each(Between(1, 9))) +``` + + +### `EachItem(key_pattern, value_pattern)` + +Matches an object if each key satisfies `key_pattern` and each value satisfies `value_pattern`. + +```python +match({"a": 1, "b": 2}, EachItem(Regex("[a-z]+"), InstanceOf(int))) +``` + + +### `Some(pattern)` (aka `Many` and `Remaining`) + +Matches a sequence of items within a list: + +```python +if result := match(range(1, 10), [1, 'a' @ Some(...), 4, 'b' @ Some(...), 8, 9]): + print(result['a']) # [2, 3] + print(result['b']) # [5, 6, 7] +``` + +Takes the optional values `exactly`, `at_least`, and `at_most` which makes `Some` match +either `exactly` _n_ items, `at_least` _n_, or `at_most` _n_ items (`at_least` and `at_most` can be given at the same +time, but not together with `exactly`). + +Note the difference between `Some(1, 2)` and `Some([1, 2])`. The first version matches subsequences, the second +version matches items which are themselves lists: + +```python +match([0, 1, 2 , 1, 2 , 3], [0, Some( 1, 2 ), 3]) # matches the subsequence 1, 2 twice +match([0, [1, 2], [1, 2], 3], [0, Some([1, 2]), 3]) # matches the item [1, 2] twice, which happen to be lists +``` + +`Some` also goes by the names of `Many` and `Remaining`, which is sometimes nice to convey meaning: + +```python +match(range(1, 10), [1, 2, 'remaining' @ Remaining()]) +match([0, 1, 1, 1, 2, 1], [0, Many(1), Remaining(InstanceOf(int))]) +``` + +When used with no arguments, `Some()` is the same as `Some(...)`. + + +### `Remainder(pattern)` + +Can be used to match the unmatched parts of a Dictionary/Mapping. + +```python +result = match({ + "foo": 1, + "bar": 2, + "qux": 4, + "quuz": 8, +}, {"foo": 'foo' @ _, "bar": 'bar' @ _} ** Remainder('rs' @ _)) +print(result.foo) # 1 +print(result.bar) # 2 +print(result.rs) # {'qux': 4, 'quuz': 8} +``` + +`Remainder` is, strictly speaking, not a `Pattern` and only works in conjunction with `**` on dictionaries, +and it only works on the right-hand side of the dictionary. + + +### `Between(lower, upper)` + +Matches an object if it is between `lower` and `upper` (inclusive). The optional keyword arguments +`lower_bound_exclusive` and `upper_bound_exclusive` can be set to `True` respectively to exclude the +lower/upper from the range of matching values. + + +### `Length(length)` + +Matches an object if it has the given length. Alternatively also accepts `at_least` and `at_most` keyword arguments. + +```python +match("abc", Length(3)) +match("abc", Length(at_least=2)) +match("abc", Length(at_most=4)) +match("abc", Length(at_least=2, at_most=4)) +``` + + +### `Contains(item)` + +Matches an object if it contains the given item (as per the same logic as the `in` operator). + +```python +match("hello there, world", Contains("there")) +match([1, 2, 3], Contains(2) & Contains(3)) +match({'foo': 1, 'bar': 2}, Contains('quux') | Contains('bar')) +``` + + +### `Regex(regex_pattern, bind_groups: bool = True)` + +Matches a string if it completely matches the given regex, as per `re.fullmatch`. +If the regular expression pattern contains named capturing groups and `bind_groups` is set to `True`, +this pattern will bind the captured results in the `MatchResult` (the default). + +To mimic `re.match` or `re.search` the given regular expression `x` can be augmented as `x.*` or `.*x.*` +respectively. + + +### `Check(predicate)` + +Matches an object if it satisfies the given predicate. + +```python +match(2, Check(lambda x: x % 2 == 0)) +``` + + +### `InstanceOf(*types)` + +Matches an object if it is an instance of any of the given types. + +```python +match(1, InstanceOf(int, flaot)) +``` + + +### `SubclassOf(*types)` + +Matches if the matched type is a subclass of any of the given types. + +```python +match(int, SubclassOf(int, float)) +``` + + +### `Parameters(...)` + +Matches the parameters of a callable. + +```python +def f(x: int, *xs: float, y: str, **kwargs: bool): + pass + + +match(f, Parameters(int, VarArgs(float), y=str, KwArgs(bool))) +``` + +Each argument to Parameters is expected to be the type of a positional argument. + +`Parameters` matches function signatures if their positional arguments match completely, i.e. + +```python +def f(x: int, y: float): + pass + + +print(bool(match(f, Parameters(int)))) # False +print(bool(match(f, Parameters(int, float)))) # True +print(bool(match(f, Parameters(int, Remaining(_))))) # True +``` + +Keyword arguments are matched only if they are keyword only arguments. In contrast to positional arguments it matches +also impartially (which aligns with the non-strict matching behavior with respect to dictionaries): + +```python +def f(x: int, *, y: str, z: float): + pass + + +print(bool(match(f, Parameters(int)))) # True +print(bool(match(f, Parameters(y=str)))) # False – positional parameters not matched +print(bool(match(f, Parameters(int, y=str)))) # True +``` + +This can be changed with `Strict`: + +```python +def f(x: int, *, y: str, z: float): + pass + + +print(bool(match(f, Strict(Parameters(int))))) # False +print(bool(match(f, Strict(Parameters(int, y=str))))) # False (z not mentioned but present) +print(bool(match(f, Strict(Parameters(int, y=str, z=float))))) # True (has y and z exactly) +``` + + +### `Arguments(*types)` + +<span style="color: red">**DEPRECATED, use `Parameters` instead (see above)**</span> + + +Matches a callable if it's type annotations correspond to the given types. + +```python +def f(x: int, y: float, z): + ... + + +match(f, Arguments(int, float, None)) +``` + +Arguments has an alternate form which can be used to match keyword arguments: + +```python + +def f(x: int, y: float, z: str): + ... + +match(f, Arguments(x=int, y=float)) +``` + +The strictness rules are the same as for dictionaries (which is why the above example works). + +```python +# given the f from above +match(f, Strict(Arguments(x=int, y=float))) # does not match +match(f, Strict(Arguments(x=int, y=float, z=str))) # matches +``` + + +### `Returns(type)` + +Matches a callable if it's type annotations denote the given return type. + +```python +def g(x: int) -> str: + ... + + +match(g, Arguments(int) & Returns(str)) +``` + + +### `Transformed(function, pattern)` + +Transforms the currently looked at value by applying `function` on it and matches the result against `pattern`. In +Haskell and other languages this is known as a [_view +pattern_](https://gitlab.haskell.org/ghc/ghc/-/wikis/view-patterns). + +```python +def sha256(v: str) -> str: + import hashlib + return hashlib.new('sha256', v.encode('utf8')).hexdigest() + +match("hello", Transformed(sha256, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")) +``` + +This is handy for matching data types like `datetime.date` as this pattern won't match if the transformation +function errored out with an exception. + +```python +from apm import * +from datetime import date + +if result := match("2020-08-27", Transformed(date.fromisoformat, 'date' @ _): + print(repr(result['date'])) # result['date'] is a datetime.date +``` + + +### `At(path, pattern)` + +Checks whether the nested object to be matched satisfies pattern at the given path. The match fails if the given path +can not be resolved. + +```python +record = { + "foo": { + "bar": { + "quux": { + "value": "deeply nested" + } + } + } +} + +result := match(record, At("foo.bar.quux", {"value": Capture(..., name="value")})) +result['value'] # "deeply nested" + +# alternate form +result := match(record, At(['foo', 'bar', 'quux'], {"value": Capture(..., name="value")})) +``` + + +### `Items(**kwargs))` + +Mostly syntactic sugar to match a dictionary nicely (and anything that provides an `.items()` method). + +```python +from apm import * +from datetime import datetime + +request = { + "api_version": "v1", + "job": { + "run_at": "2020-08-27 14:09:30", + "command": "echo 'booya'", + } +} + +if result := match(request, Items( + api_version="v1", + job=Object( + run_at=Transformed(datetime.fromisoformat, 'time' @ _), + ) & OneOf( + Items(command='command' @ InstanceOf(str)), + Items(spawn='container' @ InstanceOf(str)), + ) +)): + print(repr(result['time'])) # datetime(2020, 8, 27, 14, 9, 30) + print('container' not in result) # True + print(result['command']) # "echo 'booya'" +``` + + +### `Object(type, *args, **kwargs)` + +Matches any object of the specific type with the given attrs as in `**kwargs`. +It respects the `__match_args__` introduced by PEP-634. + +```python +from apm import * +from typing import Literal, Tuple + +class Click: + __match_args__ = ("position", "button") + + def __init__(self, pos: Tuple[int, int], btn: Literal['left', 'right', 'middle']): + self.position = pos + self.button = btn + +assert match(Click((1, 2), 'left'), Object(Click, (1, 2))) +assert match(Click((1, 2), 'left'), Object(Click, (1, 2), 'left')) +assert match(Click((1, 2), 'left'), Object(Click, (1, 2), button='left')) +``` + + +## Extensible + +New patterns can be added, just like the ones in `apm.patterns.*`. Simply extend the `apm.Pattern` class: + +```python +class Min(Pattern): + def __init__(self, min): + self.min = min + + def match(self, value, *, ctx: MatchContext, strict=False) -> MatchResult: + return ctx.match_if(value >= self.min) + +match(3, Min(1)) # matches +match(3, Min(5)) # does not match +``` + + + + +%package help +Summary: Development documents and examples for awesome-pattern-matching +Provides: python3-awesome-pattern-matching-doc +%description help +# Awesome Pattern Matching (_apm_) for Python + +[](https://github.com/scravy/awesome-pattern-matching/actions) +[](https://pepy.tech/project/awesome-pattern-matching) +[](https://pypi.org/project/awesome-pattern-matching/) + +```bash +pip install awesome-pattern-matching +``` + +- Simple +- Powerful +- Extensible +- Composable +- Functional +- Python 3.7+, PyPy3.7+ +- Typed (IDE friendly) +- Offers different styles (expression, declarative, statement, ...) + +There's a ton of pattern matching libraries available for python, all with varying degrees of maintenance and usability; +also [since Python 3.10 there is the PEP-634 `match` statement](https://www.python.org/dev/peps/pep-0634/). However, +this library still offers functionality that PEP-634 doesn't offer, as well as pattern matching for python versions +before 3.10. [A detailed comparison of PEP-634 and _`apm`_ is available](https://github.com/scravy/awesome-pattern-matching/blob/main/docs/apm_vs_pep634.md). + +_`apm`_ defines patterns as objects which are _composable_ and _reusable_. Pieces can be matched and captured into +variables, much like pattern matching in Haskell or Scala (a feature which most libraries actually lack, but which also +makes pattern matching useful in the first place - the capability to easily extract data). Here is an example: + +```python +from apm import * + +if result := match([1, 2, 3, 4, 5], [1, '2nd' @ _, '3rd' @ _, 'tail' @ Remaining(...)]): + print(result['2nd']) # 2 + print(result['3rd']) # 3 + print(result['tail']) # [4, 5] + +# If you find it more readable, '>>' can be used instead of '@' to capture a variable +match([1, 2, 3, 4, 5], [1, _ >> '2nd', _ >> '3rd', Remaining(...) >> 'tail']) +``` + +Patterns can be composed using `&`, `|`, and `^`, or via their more explicit counterparts `AllOf`, `OneOf`, and `Either` +. Since patterns are objects, they can be stored in variables and be reused. + +```python +positive_integer = InstanceOf(int) & Check(lambda x: x >= 0) +``` + +Some fancy matching patterns are available out of the box: + +```python +from apm import * + +def f(x: int, y: float) -> int: + pass + +if match(f, Arguments(int, float) & Returns(int)): + print("Function satisfies required signature") +``` + + + + +## Multiple Styles + +For matching and selecting from multiple cases, choose your style: + +```python +from apm import * + +value = 7 + +# The simple style +if match(value, Between(1, 10)): + print("It's between 1 and 10") +elif match(value, Between(11, 20)): + print("It's between 11 and 20") +else: + print("It's not between 1 and 20") + +# The expression style +case(value) \ + .of(Between(1, 10), lambda: print("It's between 1 and 10")) \ + .of(Between(11, 20), lambda: print("It's between 11 and 20")) \ + .otherwise(lambda: print("It's not between 1 and 20")) + +# The statement style +try: + match(value) +except Case(Between(1, 10)): + print("It's between 1 and 10") +except Case(Between(11, 20)): + print("It's between 11 and 20") +except Default: + print("It's not between 1 and 20") + +# The declarative style +@case_distinction +def f(n: Match(Between(1, 10))): + print("It's between 1 and 10") + +@case_distinction +def f(n: Match(Between(11, 20))): + print("It's between 11 and 20") + +@case_distinction +def f(n): + print("It's not between 1 and 20") + +f(value) + +# The terse (pampy) style +match(value, + Between( 1, 10), lambda: print("It's between 1 and 10"), + Between(11, 20), lambda: print("It's between 11 and 20"), + _, lambda: print("It's not between 1 and 20")) +``` + + +## Nested pattern matches + +Patterns are applied recursively, such that nested structures can be matched arbitrarily deep. +This is super useful for extracting data from complicated structures: + +```python +from apm import * + +sample_k8s_response = { + "containers": [ + { + "args": [ + "--cert-dir=/tmp", + "--secure-port=4443", + "--kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname", + "--kubelet-use-node-status-port" + ], + "image": "k8s.gcr.io/metrics-server/metrics-server:v0.4.1", + "imagePullPolicy": "IfNotPresent", + "name": "metrics-server", + "ports": [ + { + "containerPort": 4443, + "name": "https", + "protocol": "TCP" + } + ] + } + ] +} + +if result := match(sample_k8s_response, { + "containers": Each({ + "image": 'image' @ _, + "name": 'name' @ _, + "ports": Each({ + "containerPort": 'port' @ _ + }), + }) + }): + print(f"Image: {result['image']}, Name: {result['name']}, Port: {result['port']}") +``` + +The above will print + +``` +Image: k8s.gcr.io/metrics-server/metrics-server:v0.4.1, Name: metrics-server, Port: 4443 +``` + + +## Multimatch + +By default `match` records only the last match for captures. If for example `'item' @ InstanceOf(int)` matches multiple times, +the last match will be recorded in `result['item']`. `match` can record all captures using the `multimatch=True` flag: + +```python +if result := match([{'foo': 5}, 3, {'foo': 7, 'bar': 9}], Each(OneOf({'foo': 'item' @ _}, ...)), multimatch=True): + print(result['item']) # [5, 7] + +# The default since v0.15.0 is multimatch=False +if result := match([{'foo': 5}, 3, {'foo': 7, 'bar': 9}], Each(OneOf({'foo': 'item' @ _}, ...))): + print(result['item']) # 7 +``` + + +## Strict vs non-strict matches + +Any value which occurs verbatim in a pattern is matched verbatim (`int`, `str`, `list`, ...), except Dictionaries ( +anything which has an `items()` actually). + +Thus: + +```python +some_very_complex_object = { + "A": 1, + "B": 2, + "C": 3, +} +match(some_very_complex_object, {"C": 3}) # matches! +``` + +If you do not want unknown keys to be ignored, wrap the pattern in a `Strict`: + +```python +# does not match, only matches exactly `{"C": 3}` +match(some_very_complex_object, Strict({"C": 3})) +``` + +Lists (anything iterable which does not have an `items()` actually) are also compared as they are, i.e.: + +```python +ls = [1, 2, 3] +match(ls, [1, 2, 3]) # matches +match(ls, [1, 2]) # does not match +``` + + +## Match head and tail of a list + +It is possible to match the remainder of a list though: + +```python +match(ls, [1, 2, Remaining(InstanceOf(int))]) +``` + +And each item: + +```python +match(ls, Each(InstanceOf(int))) +``` + +Patterns can be joined using `&`, `|`, and `^`: + +```python +match(ls, Each(InstanceOf(int) & Between(1, 3))) +``` + +Wild-card matches are supported using Ellipsis (`...`): + +```python +match(ls, [1, Remaining(..., at_least=2)]) +``` + +The above example also showcases how `Remaining` can be made to match +`at_least` _n_ number of items (`Each` also has an `at_least` keyword argument). + + +## Wildcard matches anything using `_` + +A wildcard pattern can be expressed using `_`. `_` is a `Pattern` and thus `>>` and `@` can be used with it. + +```python +match([1, 2, 3, 4], [1, _, 3, _]) +``` + + +## Wildcard matches anything using `...` + +The `Ellipsis` can be used as a wildcard match, too. It is however not a `Pattern` (so `|`, `&`, `@`, etc. can not +be used on it). If you actually want to match `Ellipsis`, wrap it using `Value(...)`. + +Otherwise `...` is equivalent for most intents and purposes to `_`: + +```python +match([1, 2, 3, 4], [1, ..., 3, ...]) +``` + + +## Support for dataclasses + +```python +@dataclass +class User: + first_name: str + last_name: str + +value = User("Jane", "Doe") + +if match(value, User(_, "Doe")): + print("Welcome, member of the Doe family!") +elif match(value, User(_, _)): + print("Welcome, anyone!") +``` + + +## The different styles in detail + +### Simple style + +- 💚 has access to result captures +- 💚 vanilla python +- 💔 no case guards +- 💔 can not return values (since it's a statement, not an expression) +- 🖤 a bit repetetive +- 💚 simplest and most easy to understand style +- 🖤 fastest of them all + +```python +from apm import * + +value = {"a": 7, "b": "foo", "c": "bar"} + +if result := match(value, EachItem(_, 'value' @ InstanceOf(str) | ...), multimatch=True): + print(result['value']) # ["foo", "bar"] +``` + +#### pre `:=` version (Python 3.7) + +`bind()` can be used on a `MatchResult` to bind the matched items to an existing dictionary. + +```python +from apm import * + +value = {"a": 7, "b": "foo", "c": "bar"} + +result = {} +if match(value, EachItem(_, 'value' @ InstanceOf(str) | ...)).bind(result): + print(result['value']) # ["foo", "bar"] +elif match(value, {"quux": _ >> 'quux'}).bind(result): + print(result['quux']) +``` + +### Expression style + +- 💚 has access to result captures +- 💚 vanilla python +- 💚 can return values directly as it is an expression +- 💚 can use case guards via `when=` or `guarded` +- 🖤 so terse that it is sometimes hard to read + +The expression style is summarized: + +```python +case(value).of(pattern, action) ... .otherwise(default_action) +``` + +...where action is either a value or a callable. The captures from the matching result are bound to the named +parameters of the given callable, i.e. `result['foo']` and `result['bar']` from `'foo' @ _` and `'bar' @ _` will be +bound to `foo` and `bar` respectively in `lambda foo, bar: ...`. + +```python +from apm import * + +display_name = case({'user': 'some-user-id', 'first_name': "Jane", 'last_name': "Doe"}) \ + .of({'first_name': 'first' @ _, 'last_name': 'last' @ _}, lambda first, last: f"{first}, {last}") \ + .of({'user': 'user_id' @ _}, lambda user_id: f"#{user_id}") \ + .otherwise("anonymous") +``` + +_Note: To return a value an `.otherwise(...)` case must always be present._ + + +### Statement style + +This is arguable the most hacky style in _`apm`_, as it re-uses the `try .. except` +mechanism. It is nevertheless quite readable. + +- 💚 has access to result captures +- 💚 very readable +- 💔 can not return values (since it's a statement, not an expression) +- 💚 can use case guards via `when=` +- 🖤 misuse of the `try .. except` statement + +```python +from apm import * + +try: + match({'user': 'some-user-id', 'first_name': "Jane", 'last_name': "Doe"}) +except Case({'first_name': 'first' @ _, 'last_name': 'last' @ _}) as result: + user = f"{result['first']} {result['last']}" +except Case({'user': 'user_id' @ _}) as result: + user = f"#{result['user_id']}" +except Default: + user = "anonymous" + +print(user) # "Jane Doe" +``` + + +### Declarative style + +- 💔 does not have access to result captures +- 💚 very readable +- 💚 can use case guards via `when=` +- 💚 can return values +- 🖤 the most bloated version of all styles + +```python +from apm import * + +@case_distinction +def fib(n: Match(OneOf(0, 1))): + return n + +@case_distinction +def fib(n): + return fib(n - 2) + fib(n - 1) + +for i in range(0, 6): + print(fib(i)) +``` + +#### Nota bene: Overloading using `@case_distinction` + +If not for its pattern matching capabilities, `@case_distinction` can be used +to implement overloading. In fact, it can be imported as `@overload`. +The mechanism is aware of arity and argument types. + +```python +from apm.overload import overload + +@overload +def add(a: str, b: str): + return "".join([a, b]) + +@overload +def add(a: int, b: int): + return a + b + +add("a", "b") +add(1, 2) +``` + +### Terse style + +- 💚 has access to result captures +- 💚 can use case guards via `guarded` +- 💚 very concise +- 💚 can return values +- 🖤 very readable when formatted nicely +- 🖤 not so well suited for larger match actions +- 🖤 slowest of them all + +As the name indicates the "terse" style is terse. It is inspired by the `pampy` +pattern matching library and mimics some of its behavior. Despite a slim surface +area it also comes with some simplifications: + +- A type given as a pattern is matched against as if it was wrapped in an `InstanceOf` +- `re.Pattern` objects (result of `re.compile`) are matched against as if it was given via `Regex` +- Captures are passed to actions in the same order as they occur in the pattern (not by name) + +```python +from apm import * + +def fibonacci(n): + return match(n, + 1, 1, + 2, 1, + _, lambda x: fibonacci(x - 1) + fibonacci(x - 2) + ) + +fibonacci(6) # -> 8 + + +class Animal: pass +class Hippo(Animal): pass +class Zebra(Animal): pass +class Horse(Animal): pass + +def what_am_i(x): + return match(x, + Hippo, 'hippopotamus', + Zebra, 'zebra', + Animal, 'some other animal', + _, 'not at all an animal', + ) + +what_am_i(Hippo()) # -> 'hippopotamus' +what_am_i(Zebra()) # -> 'zebra' +what_am_i(Horse()) # -> 'some other animal' +what_am_i(42) # -> 'not at all an animal' +``` + + +## Available patterns + +### `Capture(pattern, name=<str>)` + +Captures a piece of the thing being matched by name. + +```python +if result := match([1, 2, 3, 4], [1, 2, Capture(Remaining(InstanceOf(int)), name='tail')]): + print(result['tail']) ## -> [3, 4] +``` + +As this syntax is rather verbose, two shorthand notations can be used: + +```python +# using the matrix multiplication operator '@' (syntax resembles that of Haskell and Scala) +if result := match([1, 2, 3, 4], [1, 2, 'tail' @ Remaining(InstanceOf(int))]): + print(result['tail']) ## -> [3, 4] + +# using the right shift operator +if result := match([1, 2, 3, 4], [1, 2, Remaining(InstanceOf(int)) >> 'tail']): + print(result['tail']) ## -> [3, 4] +``` + + +### `Strict(pattern)` + +Performs a strict pattern match. A strict pattern match also compares the type of verbatim values. That is, while +_`apm`_ would match `3` with `3.0` it would not do so when using `Strict`. Also _`apm`_ performs partial matches of +dictionaries (that is: it ignores unknown keys). It will perform an exact match for dictionaries using `Strict`. + +```python +# The following will match +match({"a": 3, "b": 7}, {"a": ...}) +match(3.0, 3) + +# These will not match +match({"a": 3, "b": 7}, Strict({"a": ...})) +match(3.0, Strict(3)) +``` + + +### `OneOf(*pattern)` + +Matches against any of the provided patterns. Equivalent to `p1 | p2 | p3 | ..` +(but operator overloading does not work with values that do not inherit from `Pattern`) + +```python +match("quux", OneOf("bar", "baz", "quux")) +``` + +```python +match(3, OneOf(InstanceOf(int), None)) +``` + +Patterns can also be joined using `|` to form a `OneOf` pattern: + +```python +match(3, InstanceOf(int) | InstanceOf(float)) +``` + +The above example is rather contrived, as `InstanceOf` already accepts multiple types natively: + +```python +match(3, InstanceOf(int, float)) +``` + +Since bare values do not inherit from `Pattern` they can be wrapped in `Value`: + +```python +match("quux", Value("foo") | Value("quux")) +``` + + +### `AllOf(*pattern)` + +Checks whether the value matches all of the given pattern. Equivalent to `p1 & p2 & p3 & ..` +(but operator overloading does not work with values that do not inherit from `Pattern`) + +```python +match("quux", AllOf(InstanceOf("str"), Regex("[a-z]+"))) +``` + + +### `NoneOf(*pattern)` + +Same as `Not(OneOf(*pattern))` (also `~OneOf(*pattern)`). + + +### `Not(pattern)` + +Matches if the given pattern does not match. + +```python +match(3, Not(4)) # matches +match(5, Not(4)) # matches +match(4, Not(4)) # does not match +``` + +The bitflip prefix operator (`~`) can be used to express the same thing. Note that it does not work on bare values, +so they need to be wrapped in `Value`. + +```python +match(3, ~Value(4)) # matches +match(5, ~Value(4)) # matches +match(4, ~Value(4)) # does not match +``` + +`Not` can be used do create a `NoneOf` kind of pattern: + +```python +match("string", ~OneOf("foo", "bar")) # matches everything except "foo" and "bar" +``` + +`Not` can be used to create a pattern that never matches: + +```python +Not(...) +``` + + +### `Each(pattern [, at_least=]` + +Matches each item in an iterable. + +```python +match(range(1, 10), Each(Between(1, 9))) +``` + + +### `EachItem(key_pattern, value_pattern)` + +Matches an object if each key satisfies `key_pattern` and each value satisfies `value_pattern`. + +```python +match({"a": 1, "b": 2}, EachItem(Regex("[a-z]+"), InstanceOf(int))) +``` + + +### `Some(pattern)` (aka `Many` and `Remaining`) + +Matches a sequence of items within a list: + +```python +if result := match(range(1, 10), [1, 'a' @ Some(...), 4, 'b' @ Some(...), 8, 9]): + print(result['a']) # [2, 3] + print(result['b']) # [5, 6, 7] +``` + +Takes the optional values `exactly`, `at_least`, and `at_most` which makes `Some` match +either `exactly` _n_ items, `at_least` _n_, or `at_most` _n_ items (`at_least` and `at_most` can be given at the same +time, but not together with `exactly`). + +Note the difference between `Some(1, 2)` and `Some([1, 2])`. The first version matches subsequences, the second +version matches items which are themselves lists: + +```python +match([0, 1, 2 , 1, 2 , 3], [0, Some( 1, 2 ), 3]) # matches the subsequence 1, 2 twice +match([0, [1, 2], [1, 2], 3], [0, Some([1, 2]), 3]) # matches the item [1, 2] twice, which happen to be lists +``` + +`Some` also goes by the names of `Many` and `Remaining`, which is sometimes nice to convey meaning: + +```python +match(range(1, 10), [1, 2, 'remaining' @ Remaining()]) +match([0, 1, 1, 1, 2, 1], [0, Many(1), Remaining(InstanceOf(int))]) +``` + +When used with no arguments, `Some()` is the same as `Some(...)`. + + +### `Remainder(pattern)` + +Can be used to match the unmatched parts of a Dictionary/Mapping. + +```python +result = match({ + "foo": 1, + "bar": 2, + "qux": 4, + "quuz": 8, +}, {"foo": 'foo' @ _, "bar": 'bar' @ _} ** Remainder('rs' @ _)) +print(result.foo) # 1 +print(result.bar) # 2 +print(result.rs) # {'qux': 4, 'quuz': 8} +``` + +`Remainder` is, strictly speaking, not a `Pattern` and only works in conjunction with `**` on dictionaries, +and it only works on the right-hand side of the dictionary. + + +### `Between(lower, upper)` + +Matches an object if it is between `lower` and `upper` (inclusive). The optional keyword arguments +`lower_bound_exclusive` and `upper_bound_exclusive` can be set to `True` respectively to exclude the +lower/upper from the range of matching values. + + +### `Length(length)` + +Matches an object if it has the given length. Alternatively also accepts `at_least` and `at_most` keyword arguments. + +```python +match("abc", Length(3)) +match("abc", Length(at_least=2)) +match("abc", Length(at_most=4)) +match("abc", Length(at_least=2, at_most=4)) +``` + + +### `Contains(item)` + +Matches an object if it contains the given item (as per the same logic as the `in` operator). + +```python +match("hello there, world", Contains("there")) +match([1, 2, 3], Contains(2) & Contains(3)) +match({'foo': 1, 'bar': 2}, Contains('quux') | Contains('bar')) +``` + + +### `Regex(regex_pattern, bind_groups: bool = True)` + +Matches a string if it completely matches the given regex, as per `re.fullmatch`. +If the regular expression pattern contains named capturing groups and `bind_groups` is set to `True`, +this pattern will bind the captured results in the `MatchResult` (the default). + +To mimic `re.match` or `re.search` the given regular expression `x` can be augmented as `x.*` or `.*x.*` +respectively. + + +### `Check(predicate)` + +Matches an object if it satisfies the given predicate. + +```python +match(2, Check(lambda x: x % 2 == 0)) +``` + + +### `InstanceOf(*types)` + +Matches an object if it is an instance of any of the given types. + +```python +match(1, InstanceOf(int, flaot)) +``` + + +### `SubclassOf(*types)` + +Matches if the matched type is a subclass of any of the given types. + +```python +match(int, SubclassOf(int, float)) +``` + + +### `Parameters(...)` + +Matches the parameters of a callable. + +```python +def f(x: int, *xs: float, y: str, **kwargs: bool): + pass + + +match(f, Parameters(int, VarArgs(float), y=str, KwArgs(bool))) +``` + +Each argument to Parameters is expected to be the type of a positional argument. + +`Parameters` matches function signatures if their positional arguments match completely, i.e. + +```python +def f(x: int, y: float): + pass + + +print(bool(match(f, Parameters(int)))) # False +print(bool(match(f, Parameters(int, float)))) # True +print(bool(match(f, Parameters(int, Remaining(_))))) # True +``` + +Keyword arguments are matched only if they are keyword only arguments. In contrast to positional arguments it matches +also impartially (which aligns with the non-strict matching behavior with respect to dictionaries): + +```python +def f(x: int, *, y: str, z: float): + pass + + +print(bool(match(f, Parameters(int)))) # True +print(bool(match(f, Parameters(y=str)))) # False – positional parameters not matched +print(bool(match(f, Parameters(int, y=str)))) # True +``` + +This can be changed with `Strict`: + +```python +def f(x: int, *, y: str, z: float): + pass + + +print(bool(match(f, Strict(Parameters(int))))) # False +print(bool(match(f, Strict(Parameters(int, y=str))))) # False (z not mentioned but present) +print(bool(match(f, Strict(Parameters(int, y=str, z=float))))) # True (has y and z exactly) +``` + + +### `Arguments(*types)` + +<span style="color: red">**DEPRECATED, use `Parameters` instead (see above)**</span> + + +Matches a callable if it's type annotations correspond to the given types. + +```python +def f(x: int, y: float, z): + ... + + +match(f, Arguments(int, float, None)) +``` + +Arguments has an alternate form which can be used to match keyword arguments: + +```python + +def f(x: int, y: float, z: str): + ... + +match(f, Arguments(x=int, y=float)) +``` + +The strictness rules are the same as for dictionaries (which is why the above example works). + +```python +# given the f from above +match(f, Strict(Arguments(x=int, y=float))) # does not match +match(f, Strict(Arguments(x=int, y=float, z=str))) # matches +``` + + +### `Returns(type)` + +Matches a callable if it's type annotations denote the given return type. + +```python +def g(x: int) -> str: + ... + + +match(g, Arguments(int) & Returns(str)) +``` + + +### `Transformed(function, pattern)` + +Transforms the currently looked at value by applying `function` on it and matches the result against `pattern`. In +Haskell and other languages this is known as a [_view +pattern_](https://gitlab.haskell.org/ghc/ghc/-/wikis/view-patterns). + +```python +def sha256(v: str) -> str: + import hashlib + return hashlib.new('sha256', v.encode('utf8')).hexdigest() + +match("hello", Transformed(sha256, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")) +``` + +This is handy for matching data types like `datetime.date` as this pattern won't match if the transformation +function errored out with an exception. + +```python +from apm import * +from datetime import date + +if result := match("2020-08-27", Transformed(date.fromisoformat, 'date' @ _): + print(repr(result['date'])) # result['date'] is a datetime.date +``` + + +### `At(path, pattern)` + +Checks whether the nested object to be matched satisfies pattern at the given path. The match fails if the given path +can not be resolved. + +```python +record = { + "foo": { + "bar": { + "quux": { + "value": "deeply nested" + } + } + } +} + +result := match(record, At("foo.bar.quux", {"value": Capture(..., name="value")})) +result['value'] # "deeply nested" + +# alternate form +result := match(record, At(['foo', 'bar', 'quux'], {"value": Capture(..., name="value")})) +``` + + +### `Items(**kwargs))` + +Mostly syntactic sugar to match a dictionary nicely (and anything that provides an `.items()` method). + +```python +from apm import * +from datetime import datetime + +request = { + "api_version": "v1", + "job": { + "run_at": "2020-08-27 14:09:30", + "command": "echo 'booya'", + } +} + +if result := match(request, Items( + api_version="v1", + job=Object( + run_at=Transformed(datetime.fromisoformat, 'time' @ _), + ) & OneOf( + Items(command='command' @ InstanceOf(str)), + Items(spawn='container' @ InstanceOf(str)), + ) +)): + print(repr(result['time'])) # datetime(2020, 8, 27, 14, 9, 30) + print('container' not in result) # True + print(result['command']) # "echo 'booya'" +``` + + +### `Object(type, *args, **kwargs)` + +Matches any object of the specific type with the given attrs as in `**kwargs`. +It respects the `__match_args__` introduced by PEP-634. + +```python +from apm import * +from typing import Literal, Tuple + +class Click: + __match_args__ = ("position", "button") + + def __init__(self, pos: Tuple[int, int], btn: Literal['left', 'right', 'middle']): + self.position = pos + self.button = btn + +assert match(Click((1, 2), 'left'), Object(Click, (1, 2))) +assert match(Click((1, 2), 'left'), Object(Click, (1, 2), 'left')) +assert match(Click((1, 2), 'left'), Object(Click, (1, 2), button='left')) +``` + + +## Extensible + +New patterns can be added, just like the ones in `apm.patterns.*`. Simply extend the `apm.Pattern` class: + +```python +class Min(Pattern): + def __init__(self, min): + self.min = min + + def match(self, value, *, ctx: MatchContext, strict=False) -> MatchResult: + return ctx.match_if(value >= self.min) + +match(3, Min(1)) # matches +match(3, Min(5)) # does not match +``` + + + + +%prep +%autosetup -n awesome-pattern-matching-0.24.4 + +%build +%py3_build + +%install +%py3_install +install -d -m755 %{buildroot}/%{_pkgdocdir} +if [ -d doc ]; then cp -arf doc %{buildroot}/%{_pkgdocdir}; fi +if [ -d docs ]; then cp -arf docs %{buildroot}/%{_pkgdocdir}; fi +if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi +if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi +pushd %{buildroot} +if [ -d usr/lib ]; then + find usr/lib -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/lib64 ]; then + find usr/lib64 -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/bin ]; then + find usr/bin -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/sbin ]; then + find usr/sbin -type f -printf "/%h/%f\n" >> filelist.lst +fi +touch doclist.lst +if [ -d usr/share/man ]; then + find usr/share/man -type f -printf "/%h/%f.gz\n" >> doclist.lst +fi +popd +mv %{buildroot}/filelist.lst . +mv %{buildroot}/doclist.lst . + +%files -n python3-awesome-pattern-matching -f filelist.lst +%dir %{python3_sitelib}/* + +%files help -f doclist.lst +%{_docdir}/* + +%changelog +* Thu May 18 2023 Python_Bot <Python_Bot@openeuler.org> - 0.24.4-1 +- Package Spec generated @@ -0,0 +1 @@ +7eafd9f106b6cbf0dd9f6f6ee512e5b6 awesome-pattern-matching-0.24.4.tar.gz |