Metadata-Version: 2.1
Name: awesome-pattern-matching
Version: 0.3.0
Summary: Awesome Pattern Matching
Home-page: https://github.com/scravy/awesome-pattern-matching
Author: Julian Fleischer
Author-email: tirednesscankill@warhog.net
License: UNKNOWN
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown

# Awesome Pattern Matching (_apm_) for Python

- Simple
- Powerful
- Extensible
- Python 3.8+
- Typed (IDE friendly)

There's a ton of pattern matching libraries available for python, all with varying degrees of maintenance and usability;
also [there's a PEP on it's way for a match construct](https://www.python.org/dev/peps/pep-0634/).
However, I wanted something which works well and works now, so here we are.

_`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).
Capturing pieces of the input is very similar to the way capturing groups work in regular expressions,
just a bit more noisy. Here is an example:

```python
match(value, ["first", Capture(..., name="2nd"), Capture(..., name="3rd")])
```

The above example matches a list of exactly three elements, the first element being exactly `"first"`, the seconds
and third being anything (`...` – the ellipsis is actual syntax and performs a wildcard match).
It captures the seconds and third elements as `2nd` and `3rd` respectively. `match` returns a `MatchResult` which
can be used to access `2nd` and `3rd`:

```python
if result := match(value, ["first", Capture(..., name="2nd"), Capture(..., name="3rd")]):
    result['2nd']  # first element
    result['3rd']  # second element
```

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_number = InstanceOf(int) & Check(lambda x: x >= 0)
```

## Installation

```python
pip install awesome-pattern-matching
```

## Usage

```python
from apm import *
from apm.patterns import Regex

record = {
    "ID": 9340,
    "First-Name": "Jane",
    "Last-Name": "Doe",
}

if result := match(record, {"First-Name": Capture(Regex("[A-Z][a-z]*"), name="name")}):
    print(result['name'])
```

## Very slim User Guide

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
```

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).

### `Capture(pattern, name=<str>)` (`apm.*`)

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]
```

### `Strict(pattern)` (`apm.*`)

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(pattern1, pattern2, ..)` (`apm.*`)

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))
```

### `AllOf(pattern1, pattern2, ..)` (`apm.*`)

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]+")))
```

### `Each(pattern [, at_least=]` (`apm.patterns.*`)

Matches each item in an iterable.

```python
match(range(1, 10), Each(Between(1, 9)))
```

### `EachItem(key_pattern, value_pattern)` (`apm.patterns.*`)

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)))
```

### `Check(predicate)` (`apm.patterns.*`)

Matches an object if it satisfies the given predicate.

```python
match(2, Check(lambda x: x % 2 == 0))
```

### `InstanceOf(type1 [, type2 [, ..]])` (`apm.patterns.*`)

Matches an object if it is an instance of any of the given types.

```python
match(1, InstanceOf(int, flaot))
```

### `Arguments(type1 [, type2 [, ..]])` (`apm.patterns.*`)

Matches a callable if it's type annotations correspond to the given types.
Very useful for implementing rich APIs.

```python
def f(x: int, y: float, z):
    ...

match(f, Arguments(int, float, None))
```

### `Returns(type)` (`apm.patterns.*`)

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)` (`apm.patterns.*`)

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"))
```

### `At(path, pattern)` (`apm.patterns.*`)

Checks whether the nested object to be matched satisfied 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")}))
```

## 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
```

## More Examples

Demonstrated below: Junction of Patterns using `&`, `Strict` dictionary matching, `Each`.

```python
records = [
    {
        "Foo": 1,
        "Bar": "Quux"
    },
    {
        "Foo": 2,
        "Bar": "Baz"
    }
]

assertTrue(
    match(records, Each(Strict({"Foo": InstanceOf(int), "Bar": InstanceOf(str) & Regex("[A-Z][a-z]+")}))))

records = [
    {
        "Foo": 1,
        "Bar": "Quux"
    },
    {
        "Foo": 2,
        "Bar": "Baz",
        "Strict": "Does not allow unknown keys"
    }
]

assertFalse(
    match(records, Each(Strict({"Foo": InstanceOf(int), "Bar": InstanceOf(str) & Regex("[A-Z][a-z]+")}))))

records = [
    {
        "Foo": 1,
        "Bar": "Quux"
    },
    {
        "Foo": 2,
        "Bar": "Baz",
        "No Problem": "When Not Strict"
    }
]

assertTrue(  # Note how this pattern is the same as above but without `Strict`
    match(records, Each({"Foo": InstanceOf(int), "Bar": InstanceOf(str) & Regex("[A-Z][a-z]+")})))
```


