Skip to main content

black-magic

Tests Coverage Latest Version Downloads Unlicense

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 like partial: 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.

Changelog

0.0.12

Date: 26.12.2015

  • fix missing UNLICENSE file in source distribution

  • some internal maintenance

0.0.11

Date: 25.12.2015

  • add support for python 3.5

0.0.10

  • add compatibility with python3.4

0.0.9

  • fix signature of black_magic.decorator.partial: remove named parameter func that can prevent binding of the a parameter with the same name into **kwargs

  • return value of partial can now be used for further parameter (re-)binding if the function was left open. CAREFUL: this might have unexpected characteristics, such as moving positional parameters to the right in later calls.

  • add metapartial function, that accepts only *args, **kwargs to be bound, while the function to be used can only be specified in a second step.

  • slightly improve performance of black_magic.decorator. Is now approximately the same as the performance of the decorator module.

0.0.8

  • convert all functools.partial to black_magic.partial with the same parameters.

0.0.7

  • partial support for functools.partial

0.0.6

  • fix functools.update_wrapper emulation in black_magic.decorator.wraps()

0.0.5

  • add black_magic.decorator.partial

0.0.4

  • support any callable to be passed to ASTorator.decorate

  • convert README to .rst

Metadata

Release files for black-magic 0.0.12

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for black-magic 0.0.12
File Size Uploaded Provenance Actions
black-magic-0.0.12.tar.gz 13.5 kB Without provenance Details for black-magic-0.0.12.tar.gz

Release files / black-magic-0.0.12.tar.gz

Download URL black-magic-0.0.12.tar.gz
Size 13.5 kB
Tags Source
SHA-256 checksum
How to use checksums
4d433fb9ebebf0a62ddf8108c1d21af467066de50a934a92f875058f52f44071
BLAKE2b-256 checksum
How to use checksums
bd3bde3f4022b111afae930424e96cf731754b948e4ce3d1dea83825423b2c8f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No

Security for black-magic 0.0.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page