barriers module

Python decorators for including/excluding type checks, value/bounds checks, and other code blocks within the compiled bytecode of functions and methods.

class barriers.barriers.barriers(*args: Optional[Tuple[bool]], **kwargs: Optional[Dict[str, bool]])[source]

Bases: object

Class for per-module configuration objects that can be used to define (and toggle inclusion of) categories of code blocks, to decorate functions, and to mark code blocks.

Consider the function below. The body of this function contains a code block that raises an exception if either of the two inputs is a negative integer.

>>> def f(x: int, y: int) -> int:
...
...     if x < 0 or y < 0:
...         raise ValueError('inputs must be nonnegative')
...
...     return x + y
...
>>> f(1, 2)
3
>>> f(-1, -2)
Traceback (most recent call last):
  ...
ValueError: inputs must be nonnegative

Below, an instance of barriers is introduced.

>>> from barriers import barriers
>>> example = barriers(False) @ globals()

The barriers instance example defined above is a decorator that can remove designated code blocks in the body of a function.

  • The False argument in the expression barriers(False) above should be interpreted to mean that this barrier is disabled (i.e., that the marked code blocks in the bodies of functions decorated by this decorator should be removed). The default value for this optional argument is True; this should be interpreted to mean that this barrier is enabled (and, thus, that marked code blocks should not be removed from decorated functions).

  • The notation @ globals() ensures that the namespace returned by globals is used when compiling the abstract syntax trees of transformed functions.

A code block can be designated for automatic removal by placing a marker – in this case, the example variable – on the line directly above that code block. Note that in the body of the function f defined below, the if block is immediately preceded by a line that contains the variable example.

>>> @example
... def f(x: int, y: int) -> int:
...
...     example
...     if x < 0 or y < 0:
...         raise ValueError('inputs must be nonnegative')
...
...     return x + y

The decorator @example automatically removes the if block in the function above. As a result, the function does not raise an exception when it is applied to negative inputs.

>>> f(1, 2)
3
>>> f(-1, -2)
-3

It is also possible to use the string literal 'example' as a marker.

>>> @example
... def g(x: int, y: int) -> int:
...
...     'example'
...     if x < 0 or y < 0:
...         raise ValueError('inputs must be nonnegative')
...
...     return x + y

This may be preferable because string literals appearing as statements do not contribute to the size of the compiled bytecode of a function (as shown below).

>>> def f(x: int, y: int) -> int:
...     example
...     if x < 0 or y < 0:
...         raise ValueError('inputs must be nonnegative')
...     return x + y
...
>>> def g(x: int, y: int) -> int:
...     'example'
...     if x < 0 or y < 0:
...         raise ValueError('inputs must be nonnegative')
...     return x + y
...
>>> from dis import Bytecode
>>> len(list(Bytecode(g.__code__))) < len(list(Bytecode(f.__code__)))
True

It is also possible to define and use individually named markers (which are created as attributes of the barriers instance).

>>> from barriers import barriers
>>> checks = barriers(types=True, bounds=False) @ globals()
>>> @checks
... def f(x: int, y: int) -> int:
...
...     checks.types
...     if not isinstance(x, int) and not isinstance(y, int):
...         raise TypeError('inputs must be integers')
...
...     checks.bounds
...     if x < 0 or y < 0:
...         raise ValueError('inputs must be nonnegative')
...
...     return x + y
...
>>> f('a', 'b')
Traceback (most recent call last):
  ...
TypeError: inputs must be integers
>>> f(-1, -2)
-3

When one or more named markers are defined, only named markers that have been defined can be used.

>>> @checks
... def h(x: int) -> int:
...
...     checks
...     if x == 0:
...         raise ValueError('value must be nonzero')
...
...     return x
...
Traceback (most recent call last):
  ...
RuntimeError: cannot use general marker when individual markers are defined
>>> @checks
... def h(x: int) -> int:
...
...     checks.value
...     if x == 0:
...         raise ValueError('value must be nonzero')
...
...     return x
...
Traceback (most recent call last):
  ...
NameError: marker `checks.value` is not defined
>>> @checks
... def h(x: int) -> int:
...
...     'checks.value'
...     if x == 0:
...         raise ValueError('value must be nonzero')
...
...     return x
...
Traceback (most recent call last):
  ...
NameError: marker `checks.value` is not defined

A statement may have a syntactic form that could be a marker. However, if it makes no reference to a defined instance of barriers, it is ignored.

>>> @checks
... def h(x: int) -> int:
...
...     undefined.value
...     if x == 0:
...         raise ValueError('value must be nonzero')
...
...     return x

If a string marker cannot be parsed as an expression, it is ignored.

>>> @checks
... def i(x: int, y: int) -> int:
...
...     'checks!value'
...     if x < 0 or y < 0:
...         raise ValueError('inputs must be nonnegative')
...
...     'pass'
...     if x < 0 or y < 0:
...         raise ValueError('inputs must be nonnegative')
...
...     return x + y
...
>>> i(-1, -2)
Traceback (most recent call last):
  ...
ValueError: inputs must be nonnegative

When defining individual markers, setting the status using a single boolean argument is not possible. Also, only a single boolean argument is permitted.

>>> from barriers import barriers
>>> checks = barriers(True, types=True, bounds=False)
Traceback (most recent call last):
  ...
ValueError: cannot specify general status when defining individual markers
>>> from barriers import barriers
>>> checks = barriers(True, False)
Traceback (most recent call last):
  ...
ValueError: exactly one status argument or one or more named status arguments are required

In order to accommodate the remaining examples, the statement below resets the barriers instance to one that does not define distinct, named markers.

>>> from barriers import barriers
>>> checks = barriers(False) @ globals()

Decorators can be applied to functions that invoke other functions. For example, the definition of the function f below refers to another function g.

>>> def g(x, y):
...     return x + y
...
>>> @checks
... def f(x: int, y: int) -> int:
...
...     checks
...     if x < 0 or y < 0:
...         raise ValueError('inputs must be nonnegative')
...
...     return g(x, y)
...
>>> f(1, 2)
3

For completess, the example below demonstrates that marked code blocks are by default (i.e., when no arguments are supplied to the barriers constructor) not removed.

>>> from barriers import barriers
>>> checks = barriers() @ globals()
>>> def f(x: int, y: int) -> int:
...
...     checks
...     if x < 0 or y < 0:
...         raise ValueError('inputs must be nonnegative')
...
...     return x + y
...
>>> f(-1, -2)
Traceback (most recent call last):
  ...
ValueError: inputs must be nonnegative

If an explicit attribute of the barriers instance created by the constructor is retrieved (such as in the example below where the .opt syntax appears in the expression barriers(False).opt), any function decorated by this decorator is preserved as it is (under all circumstances). Instead, the transformed version of the function is stored under the specified attribute (which in this case is opt) of the function object.

>>> from barriers import barriers
>>> checks = barriers(False).opt @ globals()

Note that in the example below, the decorator has no effect on the original function f. However, the function f.opt corresponds to the transformed version of f.

>>> def g(x, y):
...     return x + y
...
>>> @checks
... def f(x: int, y: int) -> int:
...
...     checks
...     if x < 0 or y < 0:
...         raise ValueError('inputs must be nonnegative')
...
...     return g(x, y)
...
>>> f(-1, -2)
Traceback (most recent call last):
  ...
ValueError: inputs must be nonnegative
>>> f.opt(-1, -2)
-3
__getattr__(attribute: str) barriers.barriers.barriers[source]

Set the attribute (of decorated function objects) under which the transformed versions of those functions should be stored.

>>> from barriers import barriers
>>> checks = barriers(False).opt @ globals()
>>> @checks
... def f(x: int, y: int) -> int:
...
...     checks
...     if x < 0 or y < 0:
...         raise ValueError('inputs must be nonnegative')
...
...     return x + y
...
>>> f(-1, -2)
Traceback (most recent call last):
  ...
ValueError: inputs must be nonnegative
>>> f.opt(-1, -2)
-3
__matmul__(namespace: dict) barriers.barriers.barriers[source]

Store internally the supplied namespace. This namespace is used during the compilation of transformed abstract syntax trees of functions in the _transform method.

>>> from barriers import barriers
>>> example = barriers(False) @ globals()
>>> @example
... def f(x: int, y: int) -> int:
...
...     example
...     if x < 0 or y < 0:
...         raise ValueError('inputs must be nonnegative')
...
...     return x + y
...
>>> f(-1, -2)
-3

If no namespace is specified, it is possible that instances of markers that appear in the body of a function will not be recognized.

>>> from barriers import barriers
>>> example = barriers(False)
>>> @example
... def f(x: int, y: int) -> int:
...
...     example
...     if x < 0 or y < 0:
...         raise ValueError('inputs must be nonnegative')
...
...     return x + y
...
Traceback (most recent call last):
   ...
NameError: name 'example' is not defined
__call__(function: Callable) Callable[source]

Allows this instance to behave as a decorator that parses a function and removes any marked code blocks (as specified within this instance).

>>> from barriers import barriers
>>> example = barriers(False) @ globals()
>>> @example
... def f(x: int, y: int) -> int:
...
...     example
...     if x < 0 or y < 0:
...         raise ValueError('inputs must be nonnegative')
...
...     return x + y
...
>>> f(-1, -2)
-3