Awesome Pattern Matching
Project description
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. 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:
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
:
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.
positive_number = InstanceOf(int) & Check(lambda x: x >= 0)
Installation
pip install awesome-pattern-matching
Usage
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:
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
:
# 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.:
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:
match(ls, [1, 2, Remaining(InstanceOf(int))])
And each item:
match(ls, Each(InstanceOf(int)))
Patterns can be joined using &
, |
, and ^
:
match(ls, Each(InstanceOf(int) & Between(1, 3)))
Wild-card matches are supported using Ellipsis (...
):
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.
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
.
# 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
)
match("quux", OneOf("bar", "baz", "quux"))
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
)
match("quux", AllOf(InstanceOf("str"), Regex("[a-z]+")))
Each(pattern [, at_least=]
(apm.patterns.*
)
Matches each item in an iterable.
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
.
match({"a": 1, "b": 2}, EachItem(Regex("[a-z]+"), InstanceOf(int)))
Check(predicate)
(apm.patterns.*
)
Matches an object if it satisfies the given predicate.
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.
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.
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.
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.
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.
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:
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
.
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]+")})))
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file awesome-pattern-matching-0.4.1.tar.gz
.
File metadata
- Download URL: awesome-pattern-matching-0.4.1.tar.gz
- Upload date:
- Size: 12.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.2.0 pkginfo/1.5.0.1 requests/2.24.0 setuptools/50.3.2 requests-toolbelt/0.9.1 tqdm/4.47.0 CPython/3.8.3
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 6ca84fadf6e018658a726d8a065d3f594d7b8a98b7254bca43b4a5ca3052a801 |
|
MD5 | 301c92e7b2d3ee4dd66741ba2f30efa7 |
|
BLAKE2b-256 | 810f9e4d60b2a2e539736ad21d08b84cfce2ff2b831bb21d152c27858c3144ca |
Provenance
File details
Details for the file awesome_pattern_matching-0.4.1-py3-none-any.whl
.
File metadata
- Download URL: awesome_pattern_matching-0.4.1-py3-none-any.whl
- Upload date:
- Size: 12.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.2.0 pkginfo/1.5.0.1 requests/2.24.0 setuptools/50.3.2 requests-toolbelt/0.9.1 tqdm/4.47.0 CPython/3.8.3
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 8ae776741665c159307eaa8d741bc77986e976d59a552417b59d192b53e032bc |
|
MD5 | 3dcd8e3c87305a4ac51db4bfba398e3a |
|
BLAKE2b-256 | 6ae481b50524e7c0a7df3e49dfbe76fbb4917b894aa631b563227417b092988b |