Skip to main content

Python Pattern Matching

Project description

Python Pattern Matching and Object Validation

Reusable pattern matching for Python, implemented in Cython. I originally developed this system for the Ibis Project but hopefully it can be useful for others as well.

The implementation aims to be as quick as possible, the pure python implementation is already quite fast but taking advantage of Cython allows to mitigate the overhead of the Python interpreter. I have also tried to use PyO3 but it had higher overhead than Cython. The current implementation uses the pure python mode of cython allowing quick iteration and testing, and then it can be cythonized and compiled to an extension module giving a significant speedup.

Library components

The library contains three main components which can be used independently or together:

1. Deferred object builders

These allow delayed evaluation of python expressions given a context:

In [1]: from koerce import var, resolve

In [2]: a, b = var("a"), var("b")

In [3]: expr = (a + 1) * b["field"]

In [4]: expr
Out[4]: (($a + 1) * $b['field'])

In [5]: resolve(expr, {"a": 2, "b": {"field": 3}})
Out[5]: 9

The syntax sugar provided by the deferred objects allows the definition of complex object transformations in a concise and natural way.

2. Pattern matchers which operate on various Python objects

Patterns are the heart of the library, they allow searching for specific structures in Python objects. The library provides an extensible yet simple way to define patterns and match values against them.

In [1]: from koerce import match, NoMatch, Anything

In [2]: context = {}

In [3]: match([1, 2, 3, int, "a" @ Anything()], [1, 2, 3, 4, 5], context)
Out[3]: [1, 2, 3, 4, 5]

In [4]: context
Out[4]: {'a': 5}
In [1]: from dataclasses import dataclass

In [2]: @dataclass
   ...: class B:
   ...:     x: int
   ...:     y: int
   ...:     z: float
   ...:

In [3]: match(Object(B, y=1, z=2), B(1, 1, 2))
Out[3]: B(x=1, y=1, z=2)

In [4]: Object(B, y=1, z=2)
Out[4]: ObjectOf2(<class '__main__.B'>, 'y'=EqValue(1), 'z'=EqValue(2))

where the Object pattern checks whether the passed object is an instance of B and value.y == 1 and value.z == 2 ignoring the x field.

Patterns can also be constructed from python typehints:

In [1]: from koerce import match, CoercionError

In [2]: class Ordinary:
   ...:     def __init__(self, x, y):
   ...:         self.x = x
   ...:         self.y = y
   ...:
   ...:
   ...: class Coercible(Ordinary):
   ...:
   ...:     @classmethod
   ...:     def __coerce__(cls, value):
   ...:         if isinstance(value, tuple):
   ...:             return Coercible(value[0], value[1])
   ...:         else:
   ...:             raise CoercionError("Cannot coerce value to Coercible")
   ...:

In [3]: match(Ordinary, Ordinary(1, 2))
Out[3]: <__main__.Ordinary at 0x105194fe0>

In [4]: match(Ordinary, (1, 2))
Out[4]: koerce.patterns.NoMatch

In [5]: match(Coercible, (1, 2))
Out[5]: <__main__.Coercible at 0x109ebb320>

The pattern creation logic also handles generic types by doing lightweight type parameter inference. The implementation is quite compact, available under Pattern.from_typehint().

3. A high-level validation system for dataclass-like objects

This abstraction is similar to what attrs or pydantic provide but there are some differences (TODO listing them).

In [1]: from typing import Optional
   ...: from koerce import Annotable
   ...:
   ...:
   ...: class MyClass(Annotable):
   ...:     x: int
   ...:     y: float
   ...:     z: Optional[list[str]] = None
   ...:

In [2]: MyClass(1, 2.0, ["a", "b"])
Out[2]: MyClass(x=1, y=2.0, z=['a', 'b'])

In [3]: MyClass(1, 2, ["a", "b"])
Out[3]: MyClass(x=1, y=2.0, z=['a', 'b'])

In [4]: MyClass("invalid", 2, ["a", "b"])
Out[4]: # raises validation error

Annotable object are mutable by default, but can be made immutable by passing immutable=True to the Annotable base class. Often it is useful to make immutable objects hashable as well, which can be done by passing hashable=True to the Annotable base class, in this case the hash is precomputed during initialization and stored in the object making the dictionary lookups cheap.

In [1]: from typing import Optional
   ...: from koerce import Annotable
   ...:
   ...:
   ...: class MyClass(Annotable, immutable=True, hashable=True):
   ...:     x: int
   ...:     y: float
   ...:     z: Optional[tuple[str, ...]] = None
   ...:

In [2]: a = MyClass(1, 2.0, ["a", "b"])

In [3]: a
Out[3]: MyClass(x=1, y=2.0, z=('a', 'b'))

In [4]: a.x = 2
AttributeError: Attribute 'x' cannot be assigned to immutable instance of type <class '__main__.MyClass'>

In [5]: {a: 1}
Out[5]: {MyClass(x=1, y=2.0, z=('a', 'b')): 1}

TODO:

The README is under construction, planning to improve it:

  • More advanced matching examples
  • Add benchmarks against pydantic
  • Show variable capturing
  • Show match and replace in nested structures
  • Example of validating functions by using @annotated decorator
  • Explain allow_coercible flag
  • Mention other relevant libraries

Other examples

from koerce import match, NoMatch
from koerce.sugar import Namespace
from koerce.patterns import SomeOf, ListOf

assert match([1, 2, 3, SomeOf(int, at_least=1)], four) == four
assert match([1, 2, 3, SomeOf(int, at_least=1)], three) is NoMatch

assert match(int, 1) == 1
assert match(ListOf(int), [1, 2, 3]) == [1, 2, 3]
from dataclasses import dataclass
from koerce.sugar import match, Namespace, var
from koerce.patterns import pattern
from koerce.builder import builder

@dataclass
class A:
    x: int
    y: int

@dataclass
class B:
    x: int
    y: int
    z: float


p = Namespace(pattern, __name__)
d = Namespace(builder, __name__)

x = var("x")
y = var("y")

assert match(p.A(~x, ~y) >> d.B(x=x, y=1, z=y), A(1, 2)) == B(x=1, y=1, z=2)

More examples and a comprehensive readme are on the way.

Packages are not published to PyPI yet.

Python support follows https://numpy.org/neps/nep-0029-deprecation_policy.html

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

koerce-0.1.0.tar.gz (56.9 kB view hashes)

Uploaded Source

Built Distributions

koerce-0.1.0-cp312-cp312-win_amd64.whl (420.9 kB view hashes)

Uploaded CPython 3.12 Windows x86-64

koerce-0.1.0-cp312-cp312-win32.whl (344.9 kB view hashes)

Uploaded CPython 3.12 Windows x86

koerce-0.1.0-cp312-cp312-musllinux_1_2_x86_64.whl (3.2 MB view hashes)

Uploaded CPython 3.12 musllinux: musl 1.2+ x86-64

koerce-0.1.0-cp312-cp312-musllinux_1_2_i686.whl (3.1 MB view hashes)

Uploaded CPython 3.12 musllinux: musl 1.2+ i686

koerce-0.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.6 MB view hashes)

Uploaded CPython 3.12 manylinux: glibc 2.17+ x86-64

koerce-0.1.0-cp312-cp312-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl (3.3 MB view hashes)

Uploaded CPython 3.12 manylinux: glibc 2.17+ i686 manylinux: glibc 2.5+ i686

koerce-0.1.0-cp312-cp312-macosx_14_0_arm64.whl (477.2 kB view hashes)

Uploaded CPython 3.12 macOS 14.0+ ARM64

koerce-0.1.0-cp312-cp312-macosx_13_0_x86_64.whl (523.0 kB view hashes)

Uploaded CPython 3.12 macOS 13.0+ x86-64

koerce-0.1.0-cp311-cp311-win_amd64.whl (442.6 kB view hashes)

Uploaded CPython 3.11 Windows x86-64

koerce-0.1.0-cp311-cp311-win32.whl (358.9 kB view hashes)

Uploaded CPython 3.11 Windows x86

koerce-0.1.0-cp311-cp311-musllinux_1_2_x86_64.whl (3.3 MB view hashes)

Uploaded CPython 3.11 musllinux: musl 1.2+ x86-64

koerce-0.1.0-cp311-cp311-musllinux_1_2_i686.whl (3.2 MB view hashes)

Uploaded CPython 3.11 musllinux: musl 1.2+ i686

koerce-0.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.7 MB view hashes)

Uploaded CPython 3.11 manylinux: glibc 2.17+ x86-64

koerce-0.1.0-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl (3.4 MB view hashes)

Uploaded CPython 3.11 manylinux: glibc 2.17+ i686 manylinux: glibc 2.5+ i686

koerce-0.1.0-cp311-cp311-macosx_14_0_arm64.whl (477.3 kB view hashes)

Uploaded CPython 3.11 macOS 14.0+ ARM64

koerce-0.1.0-cp311-cp311-macosx_13_0_x86_64.whl (539.4 kB view hashes)

Uploaded CPython 3.11 macOS 13.0+ x86-64

koerce-0.1.0-cp310-cp310-win_amd64.whl (438.1 kB view hashes)

Uploaded CPython 3.10 Windows x86-64

koerce-0.1.0-cp310-cp310-win32.whl (359.1 kB view hashes)

Uploaded CPython 3.10 Windows x86

koerce-0.1.0-cp310-cp310-musllinux_1_2_x86_64.whl (2.9 MB view hashes)

Uploaded CPython 3.10 musllinux: musl 1.2+ x86-64

koerce-0.1.0-cp310-cp310-musllinux_1_2_i686.whl (2.8 MB view hashes)

Uploaded CPython 3.10 musllinux: musl 1.2+ i686

koerce-0.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.3 MB view hashes)

Uploaded CPython 3.10 manylinux: glibc 2.17+ x86-64

koerce-0.1.0-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl (3.0 MB view hashes)

Uploaded CPython 3.10 manylinux: glibc 2.17+ i686 manylinux: glibc 2.5+ i686

koerce-0.1.0-cp310-cp310-macosx_14_0_arm64.whl (476.3 kB view hashes)

Uploaded CPython 3.10 macOS 14.0+ ARM64

koerce-0.1.0-cp310-cp310-macosx_13_0_x86_64.whl (537.9 kB view hashes)

Uploaded CPython 3.10 macOS 13.0+ x86-64

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