Skip to content

Commit 6eaa2ae

Browse files
authored
docs: add more pattern matching examples to the readme (#21)
1 parent a7feadb commit 6eaa2ae

File tree

9 files changed

+352
-88
lines changed

9 files changed

+352
-88
lines changed

README.md

Lines changed: 283 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ can be cythonized and compiled to an extension module giving
1515
a significant speedup. Benchmarks shows more than 2x speedup
1616
over pydantic's model validation which is written in Rust.
1717

18+
## Installation
19+
20+
The package is published to PyPI, so it can be installed using
21+
pip:
22+
23+
```sh
24+
pip install koerce
25+
```
1826

1927
## Library components
2028

@@ -64,6 +72,9 @@ In [4]: context
6472
Out[4]: {'a': 5}
6573
```
6674

75+
Note that `from koerce import koerce` function can be used instead
76+
of `match()` to avoid confusion with the built-in python `match`.
77+
6778
```py
6879
from dataclasses import dataclass
6980
from koerce import Object, match
@@ -387,20 +398,276 @@ assert match(int, 1.1) is NoMatch
387398

388399
### `If` patterns for conditionals
389400

390-
### `Custom`
401+
Allows conditional matching based on the value of the object,
402+
or other variables in the context:
403+
404+
```py
405+
from koerce import match, If, Is, var, NoMatch, Capture
406+
407+
x = var("x")
408+
409+
pattern = Capture(x) & If(x > 0)
410+
assert match(pattern, 1) == 1
411+
assert match(pattern, -1) is NoMatch
412+
```
413+
414+
### `Custom` for user defined matching logic
415+
416+
A function passed to either `match()` or `pattern()` is treated
417+
as a `Custom` pattern:
418+
419+
```py
420+
from koerce import match, Custom, NoMatch, NoMatchError
421+
422+
def is_even(value):
423+
if value % 2:
424+
raise NoMatchError("Value is not even")
425+
else:
426+
return value
427+
428+
assert match(is_even, 2) == 2
429+
assert match(is_even, 3) is NoMatch
430+
```
431+
432+
### `Capture` to record values in the context
433+
434+
A capture pattern can be defined several ways:
435+
436+
```py
437+
from koerce import Capture, Is, var
438+
439+
x = var("x")
440+
441+
Capture("x") # captures anything as "x" in the context
442+
Capture(x) # same as above but using a variable
443+
Capture("x", Is(int)) # captures only integers as "x" in the context
444+
Capture("x", Is(int) | Is(float)) # captures integers and floats as "x" in the context
445+
"x" @ Is(int) # syntax sugar for Capture("x", Is(int))
446+
+x # syntax sugar for Capture(x, Anything())
447+
```
448+
449+
```py
450+
from koerce import match, Capture, var
451+
452+
# context is a mutable dictionary passed along the matching process
453+
context = {}
454+
assert match("x" @ Is(int), 1, context) == 1
455+
assert context["x"] == 1
456+
```
457+
458+
### `Replace` for replacing matched values
459+
460+
Allows replacing matched values with new ones:
461+
462+
```py
463+
from koerce import match, Replace, var
464+
465+
x = var("x")
466+
467+
pattern = Replace(Capture(x), x + 1)
468+
assert match(pattern, 1) == 2
469+
assert match(pattern, 2) == 3
470+
```
471+
472+
there is a syntax sugar for `Replace` patterns, the example above
473+
can be written as:
474+
475+
```py
476+
from koerce import match, Replace, var
477+
478+
x = var("x")
479+
480+
assert match(+x >> x + 1, 1) == 2
481+
assert match(+x >> x + 1, 2) == 3
482+
```
483+
484+
replace patterns are especially useful when matching objects:
485+
486+
```py
487+
from dataclasses import dataclass
488+
from koerce import match, Replace, var, namespace
489+
490+
x = var("x")
491+
492+
@dataclass
493+
class A:
494+
x: int
495+
y: int
391496

392-
### `Capture`
497+
@dataclass
498+
class B:
499+
x: int
500+
y: int
501+
z: float
393502

394-
### `Replace`
503+
504+
p, d = namespace(__name__)
505+
x, y = var("x"), var("y")
506+
507+
# if value is an instance of A then capture A.0 as x and A.1 as y
508+
# then construct a new B object with arguments x=x, y=1, z=y
509+
pattern = p.A(+x, +y) >> d.B(x=x, y=1, z=y)
510+
value = A(1, 2)
511+
expected = B(x=1, y=1, z=2)
512+
assert match(pattern, value) == expected
513+
```
514+
515+
replacemenets can also be used in nested structures:
516+
517+
```py
518+
from koerce import match, Replace, var, namespace, NoMatch
519+
520+
@dataclass
521+
class Foo:
522+
value: str
523+
524+
@dataclass
525+
class Bar:
526+
foo: Foo
527+
value: int
528+
529+
p, d = namespace(__name__)
530+
531+
pattern = p.Bar(p.Foo("a") >> d.Foo("b"))
532+
value = Bar(Foo("a"), 123)
533+
expected = Bar(Foo("b"), 123)
534+
535+
assert match(pattern, value) == expected
536+
assert match(pattern, Bar(Foo("c"), 123)) is NoMatch
537+
```
395538

396539
### `SequenceOf` / `ListOf` / `TupleOf`
397540

541+
```py
542+
from koerce import Is, NoMatch, match, ListOf, TupleOf
543+
544+
pattern = ListOf(str)
545+
assert match(pattern, ["foo", "bar"]) == ["foo", "bar"]
546+
assert match(pattern, [1, 2]) is NoMatch
547+
assert match(pattern, 1) is NoMatch
548+
```
549+
398550
### `MappingOf` / `DictOf` / `FrozenDictOf`
399551

552+
```py
553+
from koerce import DictOf, Is, match
554+
555+
pattern = DictOf(Is(str), Is(int))
556+
assert match(pattern, {"a": 1, "b": 2}) == {"a": 1, "b": 2}
557+
assert match(pattern, {"a": 1, "b": "2"}) is NoMatch
558+
```
559+
400560
### `PatternList`
401561

562+
```py
563+
from koerce import match, NoMatch, SomeOf, ListOf, pattern
564+
565+
four = [1, 2, 3, 4]
566+
three = [1, 2, 3]
567+
568+
assert match([1, 2, 3, SomeOf(int, at_least=1)], four) == four
569+
assert match([1, 2, 3, SomeOf(int, at_least=1)], three) is NoMatch
570+
571+
integer = pattern(int, allow_coercion=False)
572+
floating = pattern(float, allow_coercion=False)
573+
574+
assert match([1, 2, *floating], [1, 2, 3]) is NoMatch
575+
assert match([1, 2, *floating], [1, 2, 3.0]) == [1, 2, 3.0]
576+
assert match([1, 2, *floating], [1, 2, 3.0, 4.0]) == [1, 2, 3.0, 4.0]
577+
```
578+
402579
### `PatternMap`
403580

581+
```py
582+
from koerce import match, NoMatch, Is, As
583+
584+
pattern = {
585+
"a": Is(int),
586+
"b": As(int),
587+
"c": Is(str),
588+
"d": ListOf(As(int)),
589+
}
590+
value = {
591+
"a": 1,
592+
"b": 2.0,
593+
"c": "three",
594+
"d": (4.0, 5.0, 6.0),
595+
}
596+
assert match(pattern, value) == {
597+
"a": 1,
598+
"b": 2,
599+
"c": "three",
600+
"d": [4, 5, 6],
601+
}
602+
assert match(pattern, {"a": 1, "b": 2, "c": "three"}) is NoMatch
603+
```
604+
605+
## Annotable objects
606+
607+
Annotable objects are similar to dataclasses but with some differences:
608+
- Annotable objects are mutable by default, but can be made immutable
609+
by passing `immutable=True` to the `Annotable` base class.
610+
- Annotable objects can be made hashable by passing `hashable=True` to
611+
the `Annotable` base class, in this case the hash is precomputed during
612+
initialization and stored in the object making the dictionary lookups
613+
cheap.
614+
- Validation strictness can be controlled by passing `allow_coercion=False`.
615+
When `allow_coercion=True` the annotations are treated as `As` patterns
616+
allowing the values to be coerced to the given type. When
617+
`allow_coercion=False` the annotations are treated as `Is` patterns and
618+
the values must be exactly of the given type. The default is
619+
`allow_coercion=True`.
620+
- Annotable objects support inheritance, the annotations are inherited
621+
from the base classes and the signatures are merged providing a
622+
seamless experience.
623+
- Annotable objects can be called with either or both positional and
624+
keyword arguments, the positional arguments are matched to the
625+
annotations in order and the keyword arguments are matched to the
626+
annotations by name.
627+
628+
```py
629+
from typing import Optional
630+
from koerce import Annotable
631+
632+
class MyBase(Annotable):
633+
x: int
634+
y: float
635+
z: Optional[str] = None
636+
637+
class MyClass(MyBase):
638+
a: str
639+
b: bytes
640+
c: tuple[str, ...] = ("a", "b")
641+
x: int = 1
642+
643+
644+
print(MyClass.__signature__)
645+
# (y: float, a: str, b: bytes, c: tuple = ('a', 'b'), x: int = 1, z: Optional[str] = None)
646+
647+
print(MyClass(2.0, "a", b"b"))
648+
# MyClass(y=2.0, a='a', b=b'b', c=('a', 'b'), x=1, z=None)
649+
650+
print(MyClass(2.0, "a", b"b", c=("c", "d")))
651+
# MyClass(y=2.0, a='a', b=b'b', c=('c', 'd'), x=1, z=None)
652+
653+
print(MyClass(2.0, "a", b"b", c=("c", "d"), x=2))
654+
# MyClass(y=2.0, a='a', b=b'b', c=('c', 'd'), x=2, z=None)
655+
656+
print(MyClass(2.0, "a", b"b", c=("c", "d"), x=2, z="z"))
657+
# MyClass(y=2.0, a='a', b=b'b', c=('c', 'd'), x=2, z='z')
658+
659+
MyClass()
660+
# TypeError: missing a required argument: 'y'
661+
662+
MyClass(2.0, "a", b"b", c=("c", "d"), x=2, z="z", invalid="invalid")
663+
# TypeError: got an unexpected keyword argument 'invalid'
664+
665+
MyClass(2.0, "a", b"b", c=("c", "d"), x=2, z="z", y=3.0)
666+
# TypeError: multiple values for argument 'y'
667+
668+
MyClass("asd", "a", b"b")
669+
# ValidationError
670+
```
404671

405672
## Performance
406673

@@ -450,58 +717,23 @@ advantage of the other two libraries:
450717
## TODO:
451718

452719
The README is under construction, planning to improve it:
453-
- [ ] More advanced matching examples
454-
- [ ] Add benchmarks against pydantic
455-
- [ ] Show variable capturing
456-
- [ ] Show match and replace in nested structures
457720
- [ ] Example of validating functions by using @annotated decorator
458721
- [ ] Explain `allow_coercible` flag
459-
- [ ] Mention other relevant libraries
460-
461-
## Other examples
722+
- [ ] Proper error messages for each pattern
462723

724+
## Development
463725

464-
```python
465-
from koerce import match, NoMatch
466-
from koerce.sugar import Namespace
467-
from koerce.patterns import SomeOf, ListOf
468-
469-
assert match([1, 2, 3, SomeOf(int, at_least=1)], four) == four
470-
assert match([1, 2, 3, SomeOf(int, at_least=1)], three) is NoMatch
471-
472-
assert match(int, 1) == 1
473-
assert match(ListOf(int), [1, 2, 3]) == [1, 2, 3]
474-
```
475-
476-
```python
477-
from dataclasses import dataclass
478-
from koerce.sugar import match, Namespace, var
479-
from koerce.patterns import pattern
480-
from koerce.builder import builder
481-
482-
@dataclass
483-
class A:
484-
x: int
485-
y: int
486-
487-
@dataclass
488-
class B:
489-
x: int
490-
y: int
491-
z: float
492-
493-
494-
p = Namespace(pattern, __name__)
495-
d = Namespace(builder, __name__)
496-
497-
x = var("x")
498-
y = var("y")
499-
500-
assert match(p.A(+x, +y) >> d.B(x=x, y=1, z=y), A(1, 2)) == B(x=1, y=1, z=2)
501-
```
726+
- The project uses `poetry` for dependency management and packaging.
727+
- Python version support follows https://numpy.org/neps/nep-0029-deprecation_policy.html
728+
- The wheels are built using `cibuildwheel` project.
729+
- The implementation is in pure python with cython annotations.
730+
- The project uses `ruff` for code formatting.
731+
- The project uses `pytest` for testing.
502732

503-
More examples and a comprehensive readme are on the way.
733+
More detailed developer guide is coming soon.
504734

505-
Packages are not published to PyPI yet.
735+
## References
506736

507-
Python support follows https://numpy.org/neps/nep-0029-deprecation_policy.html
737+
The project was mostly inspired by the following projects:
738+
- https://github.com/scravy/awesome-pattern-matching
739+
- https://github.com/HPAC/matchpy

0 commit comments

Comments
 (0)