Chainsmoke is a collection of tools for chains of functions. It strives to be simple, practical and well-documented.
##Logging
Here's one type of problem that Chainsmoke aims to fix:
#addition.py
import logging
def add_two_numbers(x, y):
logging.debug("[*] add_two_numbers passed arguments x: {x} and y: {y}".format(x=x, y=y))
result = x + y
logging.debug("[*] add_two_numbers return result: {result}".format(result=result))
return result
add_two_numbers(3, 4)
We want to add some debug logging, but we've unfortunately created some noise that obscures the logic. Here's a quick fix from Chainsmoke.log:
import logging
from chainsmoke.log import log_it
@log_it(logging.debug)
def add_two_numbers(x, y):
return x + y
add_two_numbers(3, 4)
This will give us the same functionality as the above example, but the logic remains clear and noise-free.
In fact, we've been able to simplify the function slightly because we do not need the intermediary result
variable.
The log_it
decorator will log the name of the function, any arguments or keyword arguments, and the results
of the computation.
For the above example the logger will create the following strings:
Input logging:
"add_two_numbers called with args: (3, 4,) and kwargs {}"
Output logging:
"add_two_numbers returned result 7"
So, now we have an function add_two_numbers
that logs its inputs and outputs. Cool, but not that cool. What's next?
##Chain
Let's set up our first chain of functions.
from chainsmoke.chain import chain
def add_two(x):
return x + 2
def multiply_by_two(x):
return x * 2
def divide_by_two(x):
return x / 2
result = chain(
5,
add_two,
multiply_by_two,
divide_by_two
)
# result: 7.0
The chain
function allows you to easily pass the result of one function to the next function. The first argument to chain
will be treated like a value, much like how 5
is in this example.
All of the functions in the chain must have an arity of one; put another way the functions in the chain must have exactly
one argument. In the real world you'll often have functions with more than one argument so when using Chainsmoke you'll find
functools.partial
comes in handy. Here's the same example using more general functions and functools.partial
:
from chainsmoke.chain import chain
from functools import partial
def addition(x, y):
return x + y
def multiplication(x, y):
return x * y
def division(x, y): # not used...
return x // y
add_two = partial(addition, 2)
multiply_by_two = partial(multiplication, 2)
divide_by_two = partial(multiplication, .5)
result = chain(
5,
add_two,
multiply_by_two,
divide_by_two
)
As you can see, functools.partial
allows you to 'bake-in' a parameter to a function; this is referred to as
'partial application' or 'currying'. Notice how we had to implement divide_by_two
as multiplication by .5?
That's due to the behavior of functools.partial
-- specifically that it replaces the arguments in order.
This means that because division is not associative you can't curry the function and get the same behavior.
There are two utilities in Chainsmoke that offer a solution to this problem; swap
and reorder
.
swap
is a simple function that swaps the order of the arguments.
from chainsmoke.functools import swap
def division(x, y):
return x // y
swapped_division = swap(division)
swapped_division(2, 4) # 2
Let's use swap on the division function in the same chain of simple arithmetic functions from before:
from functools import partial
from chainsmoke.chain import chain
from chainsmoke.functools import swap
def addition(x, y):
return x + y
def multiplication(x, y):
return x * y
def division(x, y):
return x // y
add_two = partial(addition, 2)
multiply_by_two = partial(multiplication, 2)
divide_by_two = partial(swap(division), 2)
result = chain(
5,
add_two,
multiply_by_two,
divide_by_two
)
chain
has a keyword argument wrap_with
that takes a decorator function and applies it to all of the functions in the
chain.
So instead of writing this...
from functools import partial
from chainsmoke.chain import chain
from chainsmoke.functools import swap
from chainsmoke.log import log_it
@log_it(print)
def addition(x, y):
return x + y
@log_it(print)
def multiplication(x, y):
return x * y
@log_it(print)
def division(x, y):
return x // y
add_two = partial(addition, 2)
multiply_by_two = partial(multiplication, 2)
divide_by_two = partial(swap(division), 2)
result = chain(
5,
add_two,
multiply_by_two,
divide_by_two
)
... you can write this ...
from functools import partial
from chainsmoke.chain import chain
from chainsmoke.functools import swap
from chainsmoke.log import log_it
def addition(x, y):
return x + y
def multiplication(x, y):
return x * y
def division(x, y):
return x // y
add_two = partial(addition, 2)
multiply_by_two = partial(multiplication, 2)
divide_by_two = partial(swap(division), 2)
result = chain(
5,
add_two,
multiply_by_two,
divide_by_two,
wrap_with=log_it(print)
)
Chainsmoke provides a decorator nanmed validate_it
that will do type-level validation of arguments and the return value using Python 3.5's
type annotations.
from chainsmoke.validate import validate_it
@validate_it
def add_two_numbers(x: int, y: int) -> int:
return x + y
add_two_numbers(2, 'A')
will raise a TypeError
with the following message:
"add_two_numbers expects type <class 'int'> for arg y " but received value A with type of <class 'str'>"
@validate_it
def add_two(x: int, y: int) -> int:
return str(x + y)
add_two(4, 5)
will raise a TypeError
with the following message:
add_two has return type <class 'int'> but is returning value 9 of type <class 'str'>
Any function that is passed to validate must have type annotations or an exception will be raised.
##combine
combine
allows you to combine Chainsmoke's various decorators in a way that allows them to play nicely together.
from logging import log
from chainsmoke.chain import combine
from chainsmoke.log import log_it
from chainsmoke.validate import validate_it
validate_and_log = combine(log_it(log.debug), validate_it)
@validate_and_log
def add_two(x: int, y: int) -> int:
return x + y
You can pass the decorators in any order that you like and Chainsmoke will figure out the correct order to integrate the decorators.