Skip to main content

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

awesome-pattern-matching-0.4.1.tar.gz (12.9 kB view details)

Uploaded Source

Built Distribution

awesome_pattern_matching-0.4.1-py3-none-any.whl (12.2 kB view details)

Uploaded Python 3

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

Hashes for awesome-pattern-matching-0.4.1.tar.gz
Algorithm Hash digest
SHA256 6ca84fadf6e018658a726d8a065d3f594d7b8a98b7254bca43b4a5ca3052a801
MD5 301c92e7b2d3ee4dd66741ba2f30efa7
BLAKE2b-256 810f9e4d60b2a2e539736ad21d08b84cfce2ff2b831bb21d152c27858c3144ca

See more details on using hashes here.

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

Hashes for awesome_pattern_matching-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8ae776741665c159307eaa8d741bc77986e976d59a552417b59d192b53e032bc
MD5 3dcd8e3c87305a4ac51db4bfba398e3a
BLAKE2b-256 6ae481b50524e7c0a7df3e49dfbe76fbb4917b894aa631b563227417b092988b

See more details on using hashes here.

Provenance

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page