Skip to content

jacob414/kingston

Repository files navigation

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).

Run on Repl.it

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
>>>

Aspect Oriented Programming with terse syntax

Kingston also implement a technique to do AOP with an opinionated terse syntax that I like. It lives in the kingston.aop module.

It’s used in two main ways:

With decorators

Define an Aspects object as an empty object:

>>> from kingston.aop import Aspects
>>> when = Aspects()
>>>

Then declare your aspects using the object as a decorator:

>>> @when(lambda x: x == 1, y=lambda y: y == 1)
... def labbo(x, y=1):
...     return 11
>>> @when(lambda x: x == 1, z=lambda z: z == 2)
... def labbo(x, z=2):
...     return 12
>>>

Aspect 1 above will be triggered if you call it with positional parameter 0 as 1 and a keyword parameter y=1:

>>> labbo(1, y=1)
11
>>>

Aspect 2 is triggered by parameters 1, z=2:

>>> labbo(1, z=2)
12
>>>

Any other combination of parameters will raise a AspectNotFound exception:

>>> labbo(123) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
AspectNotFound
>>>
>>>

With a mapping of aspects

You might find this better if you want brievity and/or point free style.

>>> given = Aspects({
...     (lambda x: x == 1,): lambda x: 1,
...     (lambda x: x > 1,): lambda x: x * x
... })
>>>

Calls work the same as above:

>>> given(1)
1
>>> given(2)
4
>>> given(0) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
AspectNotFound
>>>

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
>>>

Releases

No releases published

Packages

No packages published

Languages