Funcy Kingston¶
Funcy Kingston, or “Kingston” for short is a Python library with many extras that I myself find useful. It depends on the excellent library funcy. Funcy is a library to make Python programming more functional.
The best way to get started quickly is to read the README, below:
Release: v0.7.3. (Install instructions) Development: v0.7.4.
Installing Kingston¶
pip install kingston
Kingston README¶
I use the excellent Funcy library for Python a lot. This is my collection of extras that I have designed to work closely together with funcy. Funcy Kingston (Reference, see here).
Kingston is auto-formatted using yapf.
Pattern matching using extended dict
’s¶
match.Match
objects are callable objects using a dict
semantic
that also matches calls based on the type of the calling parameters:
>>> from kingston import match
>>> foo = match.TypeMatcher({
... int: lambda x: x*100,
... str: lambda x: f'Hello {x}'
... })
>>> foo(10)
1000
>>> foo('bar')
'Hello bar'
>>>
>>> from kingston import match
>>> foo = match.TypeMatcher({
... int: lambda x: x * 100,
... str: lambda x: f'Hello {x}',
... (int, int): lambda a, b: a + b
... })
>>> foo(10)
1000
>>> foo('bar')
'Hello bar'
>>>
>>> foo(1, 2)
3
>>>
You can use typing.Any
as a wildcard:
>>> from typing import Any
>>> from kingston import match
>>> foo = match.TypeMatcher({
... int: lambda x: x * 100,
... str: (lambda x: f"Hello {x}"),
... (int, Any): (lambda num, x: num * x)
... })
>>> foo(10)
1000
>>> foo('bar')
'Hello bar'
>>> foo(3, 'X')
'XXX'
>>> foo(10, 10)
100
>>>
You can also subclass type matchers and use a decorator to declare cases as methods:
>>> from kingston.match import Matcher, TypeMatcher, case
>>> from numbers import Number
>>> class NumberDescriber(TypeMatcher):
... @case
... def describe_one_int(self, one:int) -> str:
... return "One integer"
...
... @case
... def describe_two_ints(self, one:int, two:int) -> str:
... return "Two integers"
...
... @case
... def describe_one_float(self, one:float) -> str:
... return "One float"
>>> my_num_matcher:Matcher[Number, str] = NumberDescriber()
>>> my_num_matcher(1)
'One integer'
>>> my_num_matcher(1, 2)
'Two integers'
>>> my_num_matcher(1.0)
'One float'
>>>
Typing pattern matchers¶
match.Match
objects can be typed using Python’s standard
typing mechanism. It
is done using
Generics:
The two subtypes are [argument type, return type].
>>> from kingston import match
>>> foo:match.Matcher[int, int] = match.TypeMatcher({
... int: lambda x: x+1,
... str: lambda x: 'hello'})
>>> foo(10)
11
>>> foo('bar') # fails on mypy but would be ok at runtime
'hello'
>>>
Match by value(s)¶
match.ValueMatcher
will use the values of the parameters to do the
same as as match.Match
:
>>> from kingston import match
>>> foo = match.ValueMatcher({'x': (lambda: 'An x!'), ('x', 'y'): (lambda x,y: 3*(x+y))})
>>> foo('x')
'An x!'
>>> foo('x', 'y')
'xyxyxy'
>>>
Same as with the type matcher above, typing.Any
works as a wildcard
with the value matcher as well:
>>> from kingston import match
>>> from typing import Any
>>> foo = match.ValueMatcher({
... 'x': lambda x: 'An X!',
... ('y', Any): lambda x, y: 3 * (x + y)
... })
>>> foo('x')
'An X!'
>>> foo('y', 'x')
'yxyxyx'
>>>
You can also declare cases as methods in a custom ValueMatcher
subclass.
Use the function value_case()
to declare value cases. Note:
imported as a shorthand:
>>> from kingston.match import Matcher, ValueMatcher
>>> from kingston.match import value_case as case
>>> class SimplestEval(ValueMatcher):
... @case(Any, '+', Any)
... def _add(self, a, op, b) -> int:
... return a + b
...
... @case(Any, '-', Any)
... def _sub(self, a, op, b) -> int:
... return a - b
>>> simpl_eval = SimplestEval()
>>> simpl_eval(1, '+', 2)
3
>>> simpl_eval(10, '-', 5)
5
>>>
Nice things¶
dig()¶
Deep value grabbing from almost any object. Somewhat inspired by CSS selectors, but not very complete. This part of the API is unstable — it will (hopefully) be developed further in the future.
>>> from kingston import dig
>>> dig.xget((1, 2, 3), 1)
2
>>> dig.xget({'foo': 'bar'}, 'foo')
'bar'
>>> dig.dig({'foo': 1, 'bar': [1,2,3]}, 'bar.1')
2
>>> dig.dig({'foo': 1, 'bar': [1,{'baz':'jox'},3]}, 'bar.1.baz')
'jox'
>>>
The difference between dig.dig()
and funcy.get_in()
is that you
can use shell-like blob patterns to get several values keyed by similar
names:
>>> from kingston import dig
>>> res = dig.dig({'foo': 1, 'foop': 2}, 'f*')
>>> res
[foo=1:int, foop=2:int]
>>> # (textual representation of an indexable object)
>>> res[0]
foo=1:int
>>> res[1]
foop=2:int
>>>
Testing tools¶
Kingston has some testing tools as well. Also, due to Kingston’s opinionated nature, they are only targeted towards pytest.
Shortform for pytest.mark.parametrize¶
I tend to use pytest.mark.parametrize in the same form everywhere. Thus I have implemented this short-form:
>>> from kingston.testing import fixture
>>> @fixture.params(
... "a, b",
... (1, 1),
... (2, 2),
... )
... def test_dummy_compare(a, b):
... assert a == b
>>>
Doctests as fixtures¶
There is a test decorator that generates pytest fixtures from a function or an object. Use it like this:
>>> def my_doctested_func():
... """
... >>> 1 + 1
... 2
... >>> mystring = 'abc'
... >>> mystring
... 'abc'
... """
... pass
>>> from kingston.testing import fixture
>>> @fixture.doctest(my_doctested_func)
... def test_doctest_my_doctested(doctest): # fixture name always 'doctest'
... res = doctest()
... assert res == '', res
>>>
Kingston Changelog¶
0.7.0¶
- The module
kingston.match
can now do a wildcard match using...
(Ellipsis
) objects, i.e. an arbitrary long group ofAny
matchings. - Many more refactoring and fixes in
kingston.match
- Testing utility
kingston.testing.fixture
and extract and run doctests as pytest fixtures. - Dropped the homegrown pipe operator overloading mechanism, use SSPipe instead.
- Can build as a Conda Package.
- Dropped obsolete dependencies, e.g.
pysistence
. - More extensive usage of MyPy gradual typing mechanism.
0.6.8¶
- Fixes for
kingston.match
kingston.testing.trial()
/kingston.testing.retryit()
, moved tokingston.devtool
.
0.6.7¶
- Bugfix in
kingston.match.Match.case()
0.6.6¶
- Polish release, mosly QA work
- Smaller bugfixes
- Coverage analysis with pytest-cov
- Trimmed code base after coverage analysis
0.6.5¶
- New module
kingston.match
, a mechanism for ”pattern matching” using subclasses ofdict
’s to store patterns and references tocallable
’s.
0.6.4¶
- Built a more formal project structure.
- Started to use light-weight CI in the form of a GitHub action invoking Tox.
0.6.3¶
- Project renamed to ”Kingston” and re-licenced under LGPL v3
Examples¶
Tiny AST to code generation thingy¶
A key feature of Kingston is the pattern matching in kingston.match.
To get an idea of what you can build, let’s create a rudimentary AST to Python code generator. This tends to be a quite daunting exercise.
Imports needed¶
You will want to import ast
, kingston.match.Matcher
and
kingston.match.TypeMatcher
:
>>> import ast
>>> from kingston.match import Matcher, TypeMatcher
Configure a Matcher
to convert ast.AST nodes to strings¶
>>> nodeRep:Matcher[ast.AST, str] = TypeMatcher({
... ast.Interactive: lambda: '',
... ast.FunctionDef: lambda node: f"def {node.name}(",
... ast.arguments: (lambda node: ','.join(arg.arg
... for arg in node.args) +
... '):\n'),
... ast.BinOp: lambda node: '',
... ast.Constant: lambda node: str(node.value),
... ast.Return: lambda node: ' return ',
... ast.Add: lambda: ' + ',
... })
As you can see, this isn’t recursive and will be very primitive. We
will borrow ast.walk()
for brevity. Note the typing declaration at
the top line. This is to be able to use MyPy to type check our
matchings. Here the declaration means that the matcher accepts
ast.AST
nodes as parameters and will return str
.
Compile a small AST¶
>>> topnode = compile("""
... def helo():
... return 1 + 1
... """, 'examples.ormsnackis', 'single', ast.PyCF_ONLY_AST)
Given the Matcher
we just created we could only support the most
minimal AST.
Test it¶
We use the linear list of nodes you get from AST’s walk function to test without too much work:
>>> def test(tree):
... print(''.join(nodeRep(node) for node in ast.walk(tree)))
>>> if __name__ == '__main__':
... test(topnode)
Running gives the following output:
$ python examples/ormsnackis.py
def helo():
return 1 + 1
$
API Reference¶
To read about the nitty-gritty inside Kingston, you can explore the API documentation:
API Reference¶
Matching module¶
The Match module¶
This module implements a technique for pattern matching.
High-level functions¶
-
kingston.match.
matches
(values: Sequence[T_co], patterns: Sequence[T_co], matchfn: Callable = <function match>) → Union[Sequence[T_co], Type[kingston.match.Miss]][source]¶ Tries to match
values
frompatterns
.Parameters: - values – A sequence of values to match.
- patterns – A sequence of patterns that may match
values
.
Returns: The pattern that was matched or
Miss
.Return type: Union[Sequence, Type[Miss]]
High-level classes¶
-
class
kingston.match.
Matcher
[source]¶ Common base for all matcher classes.
Since
Matcher
is alsoGeneric
, you use it to subtype concrete instances of matchers you implement.
-
class
kingston.match.
TypeMatcher
[source]¶ Concrete implementation of a type matcher instance.
If you want to type a type matcher, use standard technique when using
Generic
types:>>> from kingston.match import Matcher, TypeMatcher >>> my_int_matcher:Matcher[int, int] = TypeMatcher({ ... int: lambda x: x+1, ... str: lambda x: 'str'}) >>> my_int_matcher(10) 11 >>> my_int_matcher(20) 21 >>> my_int_matcher('foo') # ok at runtime but fails mypy 'str' >>>
You can also subclass type matchers and use a decorator to declare cases as methods:
>>> from kingston.match import Matcher, TypeMatcher, case >>> from numbers import Number >>> class NumberDescriber(TypeMatcher): ... @case ... def describe_one_int(self, one:int) -> str: ... return "One integer" ... ... @case ... def describe_two_ints(self, one:int, two:int) -> str: ... return "Two integers" ... ... @case ... def describe_one_float(self, one:float) -> str: ... return "One float" >>> my_num_matcher:Matcher[Number, str] = NumberDescriber() >>> my_num_matcher(1) 'One integer' >>> my_num_matcher(1, 2) 'Two integers' >>> my_num_matcher(1.0) 'One float' >>>
-
class
kingston.match.
ValueMatcher
[source]¶ Concrete implementation of a value matching instance.
If you want to type a type matcher, use standard technique when using
Generic
types:>>> from kingston.match import ValueMatcher, Miss >>> my_val_matcher:Matcher[int, str] = ValueMatcher({ ... 1: lambda x: 'one!', ... 2: lambda x: 'two!', ... Miss: lambda x: 'many!'}) >>> my_val_matcher(1) 'one!' >>> my_val_matcher(2) 'two!' >>> my_val_matcher(3) 'many!' >>> my_val_matcher('x') # ok at runtime but fails mypy (& missleading..) 'many!' >>>
You can also declare cases as methods in a custom
ValueMatcher
subclass.Use the function
value_case()
to declare value cases. Note: imported as a shorthand:>>> from kingston.match import Matcher, ValueMatcher >>> from kingston.match import value_case as case >>> class SimplestEval(ValueMatcher): ... @case(Any, '+', Any) ... def _add(self, a, op, b) -> int: ... return a + b ... ... @case(Any, '-', Any) ... def _sub(self, a, op, b) -> int: ... return a - b >>> simpl_eval = SimplestEval() >>> simpl_eval(1, '+', 2) 3 >>> simpl_eval(10, '-', 5) 5
Exceptions and symbols¶
-
exception
kingston.match.
Conflict
[source]¶ Exception raised if a pattern to be matched already have been applied to a Matcher instance.
Low-level functions¶
-
kingston.match.
match
(cand: Any, pattern: Any) → bool[source]¶ ”Primitive” function that checks an individual value against another. Checking against
Any
works as a wildcard and will always result inTrue
.
-
kingston.match.
move
(left: Sequence[T_co], pattern: Sequence[T_co], matchfn: Callable = <function match>)[source]¶ One step of the pattern matching process. The
move()
function will take to sequences (left
,pattern
) that represents the current state of matching and produce a tuple representing the next (left
,pattern
) pair of the pattern matching.Parameters: - left – Values that haven’t been matched yet.
- pattern – Pattern values to match subsequently.
- matchfn – Function that should compare a pair of values.
Returns: A pair representing the next step in the matching process.
Return type: Tuple[Sequence,Sequence]
Dig - fetch/query in live objects¶
This module if for reading values from live objects. The general
idea is that you use a function kingston.dig.dig()
with an object
and a string. The string is (somewhat) inspired by a CSS selector. It
specifies how to get a certain sub-element.
The goal is that these spec-strings should be storable for future re-use.
This module is a work in progress module and thus subject to change.