@@ -15,6 +15,14 @@ can be cythonized and compiled to an extension module giving
1515a significant speedup. Benchmarks shows more than 2x speedup
1616over 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
6472Out[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
6879from dataclasses import dataclass
6980from 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
452719The 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