black-magic
Metaprogramming modules that operate on black magic!
Currently there is only one module available. However, I am all open for cool ideas.
black_magic.decorator
This module allows to create wrapper functions that look and behave identical to the original function. This is particularly useful for decorators.
Part of the module replicates the functionality of the well-known decorator module, but is based on creating AST nodes directly rather than composing and compiling strings.
Furthermore, this module makes it possible to create wrappers with modified
signatures. Currently, the only specialized function that is explicitly
dedicated to this purpose is partial
. If you are interested in doing
more complex modifications you can pass a dynamically created Signature
to wraps
. If you make something useful, please consider contributing
your functionality to this module.
.wraps()
wraps
can be used similarly to the standard functools.wraps
function. However, it returns a real function, i.e. something that will
have a useful signature when being inspected with help()
or by other
metaprogramming tools. Furthermore, it knows how to copy the signature
exactly, even remembering object identity of default arguments and
annotations:
>>> from black_magic.decorator import wraps
>>> def real(a=[]):
... pass
>>> @wraps(real)
... def fake(*args, **kwargs):
... return args
>>> fake()
([],)
>>> fake(1)
(1,)
>>> fake(a=2)
(2,)
>>> fake()[0] is real()
True
If you want to get real crazy you can even use ast.expr's:
>>> import ast
>>> fake = wraps(real)(ast.Num(n=1))
>>> fake(0)
1
WARNING: before using functools.partial
with any of the functions in
this module, make sure to read the warning below!
.partial()
This is similar to the functools.partial
function.
>>> from black_magic.decorator import partial
>>> def real(arg):
... print(arg)
>>> partial(real, arg=0)()
0
There are some differences, though:
-
this function returns a function object which looks like the input function, except for the modified parameters.
-
all overwritten parameters are completely removed from the signature. In
functools.partial
this is true only for arguments bound by position. -
the
**kwargs
are stripped first, then*args
>>> partial(lambda a,b,c: (a,b,c), 2, a=1)(3) (1, 2, 3)
-
by leaving the first argument empty
partial
can act as decorator:>>> @partial(None, 1, bar=0) ... def foo(bar, lum): ... return bar, lum >>> foo() (0, 1)
-
Note, that the function returned by
partial(None, ...)
is just likepartial
: it can bind additional arguments and you can still leave the first parameter unspecified. This has weird properties and should not be used in production code, but I thought it would be great to add some additional brainfuck, to see where it will go.
CAUTION: Iterative invocation of partial
(with None
as first
argument) doesn't hide parameters the same way that partial
applied to
a function does, i.e. you can move bound arguments to the right in later
calls. In code:
>>> partial(None, 1)(a=0)(lambda a, b: (a, b))()
(0, 1)
.metapartial()
The returned value can be called like partial
bind a function to the
parameters given here. In fact, partial = metapartial()
.
Binding further keyword arguments via the returned function will overwrite keyword parameters of previous bindings with the same name.
>>> @metapartial(1, a=0, c=3)
... def func(a, b, *args, **kwargs):
... return (a, b, args, kwargs)
>>> func(2)
(0, 1, (2,), {'c': 3})
.decorator()
This is the canonic utility to create decorators:
>>> from black_magic.decorator import decorator
>>> @decorator
... def plus_one(fn):
... def fake(*args, **kwargs):
... return 1 + fn(*args, **kwargs)
... return fake
>>> @plus_one
... def mul_plus_one(a, b):
... return a * b
>>> mul_plus_one(2, 3)
7
.flatorator()
flatorator
imitates the functionality of the well known decorator
module.
>>> from black_magic.decorator import flatorator
>>> @flatorator
... def times_two(fn, *args, **kwargs):
... return 2 * fn(*args, **kwargs)
>>> @times_two
... def add_times_two(a, b):
... return a + b
>>> add_times_two(1, 2)
6
Under the hood
Q: This uses ugly str
concat and eval
code, right?
A: No, it uses ugly abstract syntax tree code to do its dynamic code generation.
Q: But it's still ugly code, right?
A: Yes.
WARNING: performance hits incoming
Decorating a function with the tools in this module is a quite costy operation, so don't do it very often! Invoking the wrapper is no problem on the other hand.
WARNING: functools.partial is evil
Be careful when passing functools.partial
objects into .wraps
, or
any black magic functions more generally. functools.partial
features
very unsensible handling of arguments that are bound by keyword. These, and
all subsequent arguments, become keyword-only parameters. Consider the
following example:
>>> import functools
>>> def func(a, b, *args, **kwargs):
... return (a, b, args, kwargs)
>>> part = functools.partial(func, a=0)
>>> part(1)
Traceback (most recent call last):
...
TypeError: func() got multiple values for argument 'a'
Furthermore, note that the *args
parameter becomes completely
inaccessible, forever!
For compatibility between python versions and ease of use, I chose to handle
functools.partial
objects as if you had actually used
black_magic.decorator.partial
with the same arguments, i.e.:
>>> wrap = wraps(part)(part)
>>> wrap(1, 2, c=3)
(0, 1, (2,), {'c':3})
Note, the signature imposed by .wraps(functools.partial(f))
is
equivalent to the signature of .wraps(.partial(f))
, which might come
unexpected.
Tests
This module has been tested to work on python{2.6, 2.7, 3.3, 3.4, 3.5} and PyPy1.9 using Travis CI, and tested with python 3.2 locally.