i2.deco

Decorator tools

Functions

add_method(obj, method_func[, method_name, ...])

Dynamically add a method to an object.

assert_attrs(attrs)

Asserts, at construction time, that the class contains a specific set of attributes

double_up_as_factory(decorator_func)

Repurpose a decorator both as it's original form, and as a decorator factory.

ensure_iterable_args([func])

Wrap a function so that specific arguments are assured to be iterable if they meet specific conditions.

get_callable_from_factory_if_no_arguments(...)

Will return the input itself if it's a callable with at least one argument.

identity(obj)

Return the input unchanged.

input_output_decorator([preprocess, postprocess])

Makes a decorator that preprocesses inputs and postprocesses outputs.

is_not_set(x)

Return True if x is the NotSet sentinel, and False otherwise.

kwargs_for_func(*funcs, **kwargs)

mk_args_kwargs_merger(func)

Make a function that will return a dict containing all {argname: argval} pairs from a function's call.

mk_call_logger([logger, what_to_log, ...])

Makes a decorator that logs each call to the wrapped function.

mk_input_and_output_method_wrapper([...])

Make a method decorator transforming named arguments (arg_trans) and, if given, the output (method_output_trans).

mk_method_trans_spec_from_methods_specs_dict(...)

Utility to make inputs for wrap_class_methods_input_and_output more easily.

postprocess(post[, caught_post_errors, ...])

Add some post-processing after a function

preprocess(pre)

Make a decorator that feeds the wrapped function the output of pre.

preprocess_arguments(pre)

Make a decorator that lets pre rewrite the (args, kwargs) of every call.

transform_args([dflt_trans_func])

Make a decorator that transforms function arguments before calling the function.

transform_class_method_input_and_output(cls, ...)

Replace cls.method in place with a version whose named arguments are transformed by arg_trans and whose output by method_output_trans.

transform_instance_method_input_and_output(...)

Instance-level counterpart of transform_class_method_input_and_output; experimental (it emits a warning saying so).

transparently_wrapped(func)

Wrap func so it is called with its positional arguments packed in one tuple.

wrap_class_methods([...])

Make a decorator that wraps specific methods.

wrap_class_methods_input_and_output([...])

Make a decorator that wraps specific methods, transforming specific argument values a nd output values.

wrap_instance_methods([...])

Make a function that wraps the named methods of an instance, as wrap_class_methods_input_and_output does for a class (experimental).

wrap_method_output(wrapper_func)

Make a method decorator that applies wrapper_func to the method's output.

wraps(wrapped[, assigned, updated])

Copy of functools.wraps (kept local: it avoids a Jupyter tab-completion issue).

Classes

FuncFactory(func, *[, include, exclude])

Make a function factory.

Exceptions

OutputPostProcessingError

Raised by postprocess when the post-processing function fails.

class i2.deco.FuncFactory(func, *, include=(), exclude=())[source]

Bases: object

Make a function factory.

but more convenient and helpful (e.g. is picklable, produces functions with signatures, etc.)

One can use functools.partials to fix, or change, the defaults of arguments of a function func thereby creating a different function.

>>> def foo(a, b, *, c=2) -> float:
...     return a * b + c
>>> foo(10, 2)
22
>>> foo(10, b=2, c=3)
23
>>> from functools import partial
>>> new_foo = partial(foo, b=2, c=3)  # change default c=3 and add one: b=2
>>> new_foo(10)  # now the function can be called with one argument (couldn't before)
23

In essence, FuncFactory is equivalent to:

FuncFactory = lambda func: lambda *args, **kwargs: partial(func, *args, **kwargs)

but more convenient and helpful. For one, it doesn’t use lambda, so is picklable. It also has a more helpful signature:

>>> factory = FuncFactory(foo)
>>> factory
<FuncFactory(foo)>(a, b, *, c=2) -> ...Callable[..., float]

(Note that the repr even reuses foo’s return annotation to tell us that our factory will return a callable that returns that type (if the annotation is a type).

An instance of FuncFactory is a factory of functions, that is, it can make functions for you based on the instance’s underlying func:

>>> f = factory(b=2, c=3)
>>> f(10)
23

Note that:

>>> ff = factory(2, 3)  # equivalent to ``factory(a=2, b=3)``
>>> ff(c=10)
16

Further, you can tell FuncFactory to include or exclude specific arguments, using their names or indices to specify them.

>>> factory_no_a = FuncFactory(foo, exclude=['a'])
>>> factory_no_a
<FuncFactory(foo)>(b, *, c=2) -> ...Callable[..., float]
>>> g = factory_no_a(2, 3)  # equivalent to ``factory(b=2, c=3)`` as no ``a`` here
>>> g(10)
23

Recipe: Say you’re normalizing some data accessor into callback functions and you want to create functions that provide a specific object when called (with no args). Sure, you can do this by specifying lambda: obj every time, but lambdas can be problematic (e.g. their not picklable).

Here’s another solution:

>>> def identity(obj):
...     return obj
>>> func_returning_obj = FuncFactory(identity)
>>> get_42 = func_returning_obj(42)
>>> get_42()
42

Note

A convenience property has been added to implement this recipe:

>>> get_42, get_hello = map(FuncFactory.func_returning_obj, (42, 'hello'))
>>> get_42()
42
>>> get_hello()
'hello'
to_jdict()[source]

Return a {"func": ...} dict from which from_jdict rebuilds the factory.

classmethod wrap(include=(), exclude=())[source]

Return a FuncFactory constructor with include and exclude fixed.

exception i2.deco.OutputPostProcessingError[source]

Bases: RuntimeError

Raised by postprocess when the post-processing function fails.

i2.deco.add_method(obj, method_func, method_name=None, class_name=None)[source]

Dynamically add a method to an object.

Parameters:
  • obj – The object to add a method to

  • method_func – The function to use as a method. The first argument must be the object itself (usually called self)

  • method_name – The desired function name. If None, will take method_func.__name__

  • class_name – The desired class name. If None, will take type(obj).__name__

Returns:

the object, but with the additional method (or a different function for it)

>>> class A:
...     def __init__(self, x=10):
...         self.x = x
>>> def times(self, y):
...     return self.x * y
>>> def plus(self, y):
...     return self.x + y
>>> a = A(x=10)
>>> a = add_method(a, plus, '__call__')  # add a __call__ method, assigning it to plus
>>> a(2)
12
>>> a = add_method(a, times, '__call__')  # reassign the __call__ method to times instead
>>> a(2)
20
>>> a = add_method(a, plus, '__getitem__')  # assign the method __getitem__ to plus
>>> a[2]  # see that it works
12
>>> a(2)  # and that we still have our __call__ method
20
i2.deco.assert_attrs(attrs)[source]

Asserts, at construction time, that the class contains a specific set of attributes

Parameters:

attrs – An attribute name (string) or a list of attribute names whose existence needs to be enforced.

Returns:

A class decorator that will enforce the existence of the attrs when an instance is made

>>> @assert_attrs('foo')
... class A:
...     bar = 10
...
>>> try:
...     a = A()
... except AttributeError:
...     print("AttributeError, as expected, because missing the foo attribute")
AttributeError, as expected, because missing the foo attribute
>>> @assert_attrs('foo')
... class B:
...     def foo(self): pass
>>> b = B()
>>>
>>> class A:
...     bar = 10
>>> class B:
...     def foo(self): pass
>>>
>>> @assert_attrs(['foo', 'bar'])
... class C(A, B):
...     pass
>>> c = C()
i2.deco.double_up_as_factory(decorator_func)[source]

Repurpose a decorator both as it’s original form, and as a decorator factory. That is, from a decorator that is defined do wrapped_func = decorator(func, **params), make it also be able to do wrapped_func = decorator(**params)(func).

Note

You’ll only be able to do this if all but the first argument are keyword-only, and the first argument (the function to decorate) has a default of None (this is for your own good). This is validated before making the “double up as factory” decorator.

>>> @double_up_as_factory
... def decorator(func=None, *, multiplier=2):
...     def _func(x):
...         return func(x) * multiplier
...     return _func
...
>>> def foo(x):
...     return x + 1
...
>>> foo(2)
3
>>> wrapped_foo = decorator(foo, multiplier=10)
>>> wrapped_foo(2)
30

The object to wrap doesn’t have to be given positionally: it can also be given by keyword, under the name the decorator gave its first parameter (here, func). This matters because forwarding arguments through **kwargs is a very common way to call a decorator, so decorator(func=foo) must mean what decorator(foo) means:

>>> decorator(func=foo, multiplier=10)(2)
30
>>> decorator(func=foo)(2)
6

It is the absence of an object to wrap – not the way it’s passed – that asks for a factory:

>>> from functools import partial
>>> isinstance(decorator(multiplier=3), partial)
True
>>> isinstance(decorator(func=foo), partial)
False
>>> multiply_by_3 = decorator(multiplier=3)
>>> wrapped_foo = multiply_by_3(foo)
>>> wrapped_foo(2)
9
>>>
>>> @decorator(multiplier=3)
... def foo(x):
...     return x + 1
...
>>> foo(2)
9

Note that to be able to use double_up_as_factory, your first argument (the object to be wrapped) needs to default to None and be the only argument that is not keyword-only (i.e. all other arguments need to be keyword only).

>>> @double_up_as_factory
... def decorator_2(func, *, multiplier=2):
...     '''Should not be able to be transformed with double_up_as_factory'''
Traceback (most recent call last):
  ...
AssertionError: First argument of the decorator function needs to default to None. Was <class 'inspect._empty'>
>>> @double_up_as_factory
... def decorator_3(func=None, multiplier=2):
...     '''Should not be able to be transformed with double_up_as_factory'''
Traceback (most recent call last):
  ...
AssertionError: All arguments (besides the first) need to be keyword-only

Note also that the name of that first argument is effectively reserved: it always means “the object to wrap”. For a decorator that also takes **kwargs, this means a decorator argument can never share that name. Say a decorator’s first parameter is func and it renames parameters via **kwargs:

>>> @double_up_as_factory
... def rename(func=None, **new_name_for_old_name):
...     return new_name_for_old_name  # (stand-in for the real work)

You can rename an ordinary parameter through the factory form:

>>> rename(b='bee')(lambda a, b: None)
{'b': 'bee'}

But you cannot use it to rename a parameter that happens to be called func: rename(func='callback') is read as “wrap the object 'callback'”, not as “rename func to callback”, so it returns nonsense rather than a factory:

>>> rename(func='callback')
{}

This is a pre-existing limitation of the double-up idiom – there is no way to tell the two intents apart – and it is not specific to passing the object by keyword. Before keyword-passing was supported the same call failed later and differently, with TypeError: rename() got multiple values for argument 'func'. If a decorator needs an argument with the same name as its first parameter, don’t use double_up_as_factory.

i2.deco.ensure_iterable_args(func=None, **condition_of_argname)[source]

Wrap a function so that specific arguments are assured to be iterable if they meet specific conditions.

The condition, in the example below, is being a string. Note that in general, the condition needs to be a boolean function. The explicit form of our example would say names=lambda x: isinstance(x, str), but ensure_iterable_args allows the convenience of just specifying the type, or a tuple of types, and the actually boolean function will be made for you.

>>> @ensure_iterable_args(names=str)
... def greet_people(names, greeting='Hello'):
...     for name in names:
...         yield f"{greeting} {name}!"
>>> assert list(greet_people(['Alice', 'Bob'])) == ['Hello Alice!', 'Hello Bob!']
>>> assert list(greet_people('Alice')) == ['Hello Alice!']

Note that to decorate a function, you can also use the form:

>>> greet_people = ensure_iterable_args(greet_people, names=str)
i2.deco.get_callable_from_factory_if_no_arguments(func_or_factory_thereof)[source]

Will return the input itself if it’s a callable with at least one argument. If not, it will consider it to be a factory, call it to get the actual callable object that the user presumably is seeking to get

i2.deco.identity(obj)[source]

Return the input unchanged.

Return type:

TypeVar(T)

i2.deco.input_output_decorator(preprocess=None, postprocess=None)[source]

Makes a decorator that preprocesses inputs and postprocesses outputs. Use it if you want to transform the input of a function or method before calling it, or if you want to transform the returned value before returning it.

Parameters:
  • preprocess – Function to be applied to input

  • postprocess – Function to be applied to output

Returns:

a decorator that preprocesses inputs and postprocesses outputs

See also

preprocess and postprocess decorators if you need only to pre or post process!

>>> # Examples with "normal functions"
>>> def f(x=3):
...     '''Some doc...'''
...     return x + 10
>>> ff = input_output_decorator()(f)
>>> print((ff(5.0)))
15.0
>>> ff = input_output_decorator(preprocess=int)(f)
>>> print((ff(5.0)))
15
>>> ff = input_output_decorator(preprocess=int, postprocess=lambda x: "Hello {}!".format(x))(f)
>>> print((ff('5')))
Hello 15!
>>> ff = input_output_decorator(postprocess=lambda x: "Hello {}!".format(x))(f)
>>> print((ff(5.0)))
Hello 15.0!
>>> print((ff.__doc__))
Some doc...
>>>
>>> # examples with methods (bounded, class methods, static methods
>>> class F:
...     '''This is not what you'd expect: The doc of the class, not the function'''
...     def __init__(self, y=10):
...         '''Initialize'''
...         self.y = y
...     def __call__(self, x=3):
...         '''Some doc...'''
...         return self.y + x
...     @staticmethod
...     def static_method(x, y):
...         return "What {} {} you have".format(x, y)
...     @classmethod
...     def class_method(cls, x):
...         return "{} likes {}".format(cls.__name__, x)
>>>
>>> f = F()
>>> ff = input_output_decorator()(f)
>>> print((ff(5.0)))
15.0
>>> ff = input_output_decorator(preprocess=int)(f)
>>> print((ff(5.0)))
15
>>> ff = input_output_decorator(preprocess=int, postprocess=lambda x: "Hello {}!".format(x))(f)
>>> print((ff('5')))
Hello 15!
>>> ff = input_output_decorator(postprocess=lambda x: "Hello {}!".format(x))(f)
>>> print((ff(5.0)))
Hello 15.0!
>>> print((ff.__doc__))
This is not what you'd expect: The doc of the class, not the function

# >>>

i2.deco.is_not_set(x)[source]

Return True if x is the NotSet sentinel, and False otherwise.

Signature consumers (UI or schema generators, for example) can use it to treat a NotSet default like inspect.Parameter.empty, i.e. “required, no default”:

Return type:

bool

>>> from inspect import Parameter
>>> is_not_set(NotSet)
True
>>> is_not_set(None), is_not_set(Parameter.empty), is_not_set("NotSet")
(False, False, False)
>>> def default_or_empty(param):
...     return Parameter.empty if is_not_set(param.default) else param.default
>>> p = Parameter('x', Parameter.KEYWORD_ONLY, default=NotSet)
>>> default_or_empty(p) is Parameter.empty
True
i2.deco.kwargs_for_func(*funcs, **kwargs)[source]
Parameters:
  • funcs

  • kwargs

Returns:

>>> from i2.tests.objects_for_testing import formula1, sum_of_args, mult, add
>>> def print_dict(d):  # just a util for this doctest
...     from pprint import pprint
...     pprint({k.__name__: d[k] for k in sorted(d, key=lambda x: x.__name__)})
>>> print_dict(kwargs_for_func(formula1, mult, add,
...                           w=1, x=2, z=3, a=4, b=5))
{'add': {'a': 4, 'b': 5},
 'formula1': {'w': 1, 'x': 2, 'z': 3},
 'mult': {'x': 2}}
i2.deco.mk_args_kwargs_merger(func)[source]

Make a function that will return a dict containing all {argname: argval} pairs from a function’s call. That is, it merges all non-keyword arguments with the keyword-arguments, with the right name, so that the arguments can be handled more uniformly.

Parameters:

func – The function that will be called, whose signature should be looked at to make the merging function

Returns:

A function merge_args_and_kwargs(args, kwargs) that can be used to merge arguments

>>> def func(a, b, c=3):
...     return a * (b + c)
>>> merger = mk_args_kwargs_merger(func)
>>> dict(merger([1], {'b': 10}))
{'a': 1, 'b': 10}
>>> dict(merger([], {'a': 1, 'b': 10}))
{'a': 1, 'b': 10}
>>> dict(merger([1, 10], {}))
{'a': 1, 'b': 10}
>>> dict(merger([], {}))
{}
>>> # Usage demo:
>>> assert func(*[1], **{'b': 10}) == func(**merger([1], {'b': 10}))
>>> assert func(*[], **{'a': 1, 'b': 10}) == func(**merger([], {'a': 1, 'b': 10}))
>>> assert func(**{'a': 1, 'b': 10}) == func(**merger([], {'a': 1, 'b': 10}))
i2.deco.mk_call_logger(logger=<built-in function print>, what_to_log=<function _call_signature>, log_output=False, func_is_bounded=False)[source]

Makes a decorator that logs each call to the wrapped function.

Parameters:
  • logger – The actual function that logs stuff. Default is print. The “stuff” it logs is given by the what_to_log argument (a function).

  • what_to_log (Callable[[Callable, tuple, dict], Any]) – A function taking inputs (func, args, kwargs) of the call, and returning something to log (usually, and by default, a string)

  • func_is_bounded – Whether the function is bounded (like a method) or not

Returns:

A decorator

>>> # Example of use on (unbounded) function, with default args
>>> @mk_call_logger()
... def useless_computation(x, y=2, z='foo'):
...     return z * (x + y)
...
>>> _ = useless_computation(3, y=1, z='ha')
useless_computation(3, y=1, z='ha')

The same example, but with output logging too

>>> @mk_call_logger(log_output=True)
... def useless_computation(x, y=2, z='foo'):
...     return z * (x + y)
>>> _ = useless_computation(3, y=1, z='ha')
useless_computation(3, y=1, z='ha')
-> hahahaha

And now a bit more involved…

>>>
>>> # Example of use on class method, with a different what_to_log function.
>>> class A:
...     def __init__(self, a=10):
...         self.a = a
...     def add(self, x):
...         return self.a + x
...     def multiply(self, x):
...         return self.a * x
...
>>> def _name_args_kwargs(func, args, kwargs) -> str:
...     return "Calling {} with\n  args={}\n  kwargs={}".format(func.__name__, args, kwargs)
...
>>>
>>> log_calls = mk_call_logger(what_to_log=_name_args_kwargs, func_is_bounded=True)
>>> for method in ['add', 'multiply']:
...     A_method = getattr(A, method)
...     setattr(A, method, mk_call_logger(what_to_log=_name_args_kwargs, func_is_bounded=True)(A_method))
...
>>>
>>> a = A()
>>> a.add(x=2)
Calling add with
  args=()
  kwargs={'x': 2}
12
>>> a.multiply(2)
Calling multiply with
  args=(2,)
  kwargs={}
20
i2.deco.mk_input_and_output_method_wrapper(method_output_trans=None, **arg_trans)[source]

Make a method decorator transforming named arguments (arg_trans) and, if given, the output (method_output_trans).

i2.deco.mk_method_trans_spec_from_methods_specs_dict(methods_specs_dict)[source]

Utility to make inputs for wrap_class_methods_input_and_output more easily.

Parameters:

methods_specs_dict – a dict where keys are method names (either a single string, or a tuple of strings) values are the trans_spec dicts that should be associated to those methods

Returns:

A dict in the method_trans_spec (input of wrap_class_method) format.

>>> methods_specs_dict = {}
>>> methods_specs_dict['foo'] = {'x': str, 'y': int}
>>> methods_specs_dict[('foo', 'bar')] = {'z': list, 'method_output_trans': float}
>>> methods_specs_dict[('bar', )] = {'zz': int}
>>> method_trans_spec = mk_method_trans_spec_from_methods_specs_dict(methods_specs_dict)
>>> list(method_trans_spec.keys())
['foo', 'bar']
>>> method_trans_spec['foo']
{'x': <class 'str'>, 'y': <class 'int'>, 'z': <class 'list'>, 'method_output_trans': <class 'float'>}
>>> method_trans_spec['bar']
{'z': <class 'list'>, 'method_output_trans': <class 'float'>, 'zz': <class 'int'>}
i2.deco.postprocess(post, caught_post_errors=(<class 'Exception'>, ), verbose_error_message=False)[source]

Add some post-processing after a function

Parameters:

post – The function to apply to the output

>>> list_range = postprocess(list)(range)
>>> list_range(4)
[0, 1, 2, 3]
>>> sum_range = postprocess(sum)(range)
>>> sum_range(4)
6

Note

The decorator also sticks the return annotation of the post function on the wrapped one.

Use cases:

  • Changing a generator into a container returning function In many situations, writing a generator is simpler than writing a function that accumulates a list or a dict etc. So here, you just write the generator and tag this decorator on top, to get the same effect.

>>> from inspect import signature
>>> @postprocess(dict)
... def bar(x):
...     for i in range(x):
...         yield str(i), i
>>> bar(3)
{'0': 0, '1': 1, '2': 2}
>>> signature(bar)
<Signature (x) -> dict>
>>>
>>> @postprocess(list)
... def foo(x):
...     for i in range(x):
...         yield i
>>> foo(3)
[0, 1, 2]
>>> from inspect import signature
>>> signature(foo)
<Signature (x) -> list>
  • Triggering something (like logging, or forwarding) when a function returns

>>> def log_this(x):
...     print(f"Logging {x}")
...     return x
>>> logged_foo = postprocess(log_this)(foo)
>>> t = logged_foo(2)
Logging [0, 1]
>>> assert t == [0, 1]
  • Using a function that does a lot to make several functions that do less. (e.g. Extracting/making a python object from a function returning a raw http response)

i2.deco.preprocess(pre)[source]

Make a decorator that feeds the wrapped function the output of pre.

The wrapped function receives a single argument: pre(*args, **kwargs), computed from whatever the caller passed.

>>> @preprocess(int)
... def double(x):
...     return 2 * x
>>> double("21")
42

See also

postprocess: apply a function to the output instead. preprocess_arguments: pre returns the (args, kwargs) pair to call the wrapped function with, instead of a single value.

i2.deco.preprocess_arguments(pre)[source]

Make a decorator that lets pre rewrite the (args, kwargs) of every call.

pre(*args, **kwargs) must return an (args, kwargs) pair; the wrapped function is then called with that pair.

>>> @preprocess_arguments(lambda *args, **kwargs: (
...     tuple(int(a) for a in args), {k: int(v) for k, v in kwargs.items()}
... ))
... def add(a, b):
...     return a + b
>>> add("1", b="2")
3

See also

preprocess: pre returns a single value that becomes the only argument. transform_args: transform named arguments one by one.

i2.deco.transform_args(dflt_trans_func=None, /, **trans_func_for_arg)[source]

Make a decorator that transforms function arguments before calling the function. Works with plain functions and bounded methods. For example:

  • original argument: a relative path –> used argument: a full path

  • original argument: a pickle filepath –> used argument: the loaded object

Parameters:
  • rootdir – rootdir to be used for all name arguments of target function

  • name_arg – the position (int) or argument name of the argument containing the name

Returns:

a decorator

>>> # Example with a plain function
>>> def f(a, b, c='default_c'):
...     return "a={a}, b={b}, c={c}".format(a=a, b=b, c=c)
>>> def prepend_root(x):
...     return 'ROOT/' + x
>>>
>>> def test(f):
...     assert f('foo', 'bar', 3) == 'a=foo, b=bar, c=3'
...     ff = transform_args()(f)  # no transformation specification, so function is unchanged
...     assert ff('foo', 'bar', c=3) == 'a=foo, b=bar, c=3'
...     ff = transform_args(a=prepend_root)(f)  # prepend root to a
...     assert ff('foo', c=3, b='bar') == 'a=ROOT/foo, b=bar, c=3'  # note: testing different order of args
...     ff = transform_args(b=prepend_root)(f)  # prepend root to b
...     assert ff(c=3, b='bar', a='foo') == 'a=foo, b=ROOT/bar, c=3'  # note: testing different order of args
...     ff = transform_args(a=prepend_root, b=prepend_root)(f)  # prepend root to a and b
...     assert ff('foo', 'bar', 3) == 'a=ROOT/foo, b=ROOT/bar, c=3'
...     assert ff('foo', 'bar') == 'a=ROOT/foo, b=ROOT/bar, c=default_c'  # defaults still work
>>>
>>> test(f)
>>>
>>> # Example with bounded method, wrapping from instance
>>> class A:
...     def __init__(self, sep=''):
...         self.sep = sep
...     def f(self, a, b, c='default_c'):
...         return f"a={a}{self.sep} b={b}{self.sep} c={c}"
>>>
>>> a = A(sep=',')
>>> test(a.f)
>>>
>>> # Example with bounded method, wrapping from class
>>> A.f = transform_args(a=prepend_root, b=prepend_root)(A.f)
>>> a = A(sep=',')
>>> assert a.f('foo', 'bar', 3) == 'a=ROOT/foo, b=ROOT/bar, c=3'
>>> assert a.f('foo', 'bar') == 'a=ROOT/foo, b=ROOT/bar, c=default_c'  # defaults still work
i2.deco.transform_class_method_input_and_output(cls, method, method_output_trans=None, **arg_trans)[source]

Replace cls.method in place with a version whose named arguments are transformed by arg_trans and whose output by method_output_trans.

i2.deco.transform_instance_method_input_and_output(obj, method, method_output_trans=None, **arg_trans)[source]

Instance-level counterpart of transform_class_method_input_and_output; experimental (it emits a warning saying so).

i2.deco.transparently_wrapped(func)[source]

Wrap func so it is called with its positional arguments packed in one tuple.

i2.deco.wrap_class_methods(_return_a_copy_of_the_class=True, _raise_error_if_non_existent_method=True, **wrapper_for_method)[source]

Make a decorator that wraps specific methods.

Important

The decorator will by default return a copy of the class. This might incur some run time overhead. If this is desirable, for example, when you want to create several decorations of a same class. If you want to change the class itself (e.g. you’re only loading it once in a module, and decorating it), then specify _return_a_copy_of_the_class=False

Note that _return_a_copy_of_the_class=True has a side effect of building russian dolls of essentially subclasses of the class, which may have some undesirable results if repeated too many times.

Parameters:
  • _return_a_copy_of_the_class – Specifies whether to return a copy of the class (_return_a_copy_of_the_class=True, the default), or change the actual loaded class itself (_return_a_copy_of_the_class=False)

  • wrapper_for_method – method_name=wrapper_function pairs.

Returns:

A class wrapper. That is, a decorator that takes a class and returns a decorated version of it (or decaorates “in-place” if _return_a_copy_of_the_class=False

See also

  • wrap_method_output: The function that is called for every method we wrap.

  • transform_class_method_input_and_output: A wrap_class_methods that is specialized for input arg and output

    transformation.

>>> from functools import wraps
>>> class A:
...     def __init__(self, a=10):
...         self.a = a
...     def add(self, x):
...         return self.a + x
...     def multiply(self, x):
...         return self.a * x
...
>>> a = A()
>>> a.add(2)
12
>>> a.multiply(2)
20
>>>
>>> def log_calls(func):
...     name = func.__name__
...     @wraps(func)
...     def _func(self, *args, **kwargs):
...         print("Calling {} with\n  args={}\n  kwargs={}".format(name, args, kwargs))
...         return func(self, *args, **kwargs)
...     return _func
...
>>> AA = wrap_class_methods(**{k: log_calls for k in ['add', 'multiply']})(A)
>>> a = AA()
>>> a.add(x=3)
Calling add with
  args=()
  kwargs={'x': 3}
13
>>> a.multiply(3)
Calling multiply with
  args=(3,)
  kwargs={}
30
i2.deco.wrap_class_methods_input_and_output(_return_a_copy_of_the_class=True, _raise_error_if_non_existent_method=True, **method_trans_spec)[source]

Make a decorator that wraps specific methods, transforming specific argument values a nd output values.

Important

The decorator will by default return a copy of the class. This might incur some run time overhead. If this is desirable, for example, when you want to create several decorations of a same class. If you want to change the class itself (e.g. you’re only loading it once in a module, and decorating it), then specify _return_a_copy_of_the_class=False

Parameters:
  • _return_a_copy_of_the_class – Specifies whether to return a copy of the class (_return_a_copy_of_the_class=True, the default), or change the actual loaded class itself (_return_a_copy_of_the_class=False)

  • method_trans_spec – method_name=trans_specs_for_method pairs. The trans_specs_for_method is a dict that is understood by transform_class_method_input_and_output. Except for one special case, it’s keys are argument names and values are callables to call on those arguments’ values. The special case is method_output_trans. This specifies that the callable it points to should be called on output of method. Here’s one recipe for outputs: If the output of a function is an iterable and you want to apply a function trans to each element of the output, specify method_output_trans=lambda x: map(trans, x).

Returns:

A wrapped class

See also

  • mk_method_trans_spec_from_methods_specs_dict: a utility to make method_trans_spec more easily

  • transform_class_method_input_and_output: The function that is called for every method we wrap.

In the following, we will show two examples.

  • The first is a toy example to demonstrate the basic functionality.

  • The second demonstrates a more involved case, but is still a silly example.

  • The third demonstrates more the type of application we’d use wrap_class_methods_input_and_output for in real life.

FIRST EXAMPLE

We make an Ops class that wraps Counter, allowing one to add items and show the counts of items added.

>>> from collections import UserDict
>>> import re
>>> from collections import Counter
>>>
>>> class Ops:
...     def __init__(self):
...         self.counter = Counter()
...     def add_item(self, item):
...         self.counter.update({item: 1})
...     def show(self):
...         return self.counter
>>> # Here's an example of what Ops does
>>> ops = Ops()
>>> for item in ['this', 'is', 'that', 'and', 'that', 'is', 'this']:
...     ops.add_item(item)
...
>>> ops.show()
Counter({'this': 2, 'is': 2, 'that': 2, 'and': 1})
>>>
>>> # But say we don't want to count actual words added, but just the first two letters of these words,
>>> # and say we want to show() to return the dict, not the Counter.
>>> NewOps = wrap_class_methods_input_and_output(
...     _return_a_copy_of_the_class=False,
...     add_item=dict(item=lambda x: x[:2]),  # intercept items fed to add_item and keep only 2 first letters
...     show=dict(method_output_trans=dict)  # intercept output of show method, converting to dict
... )(Ops)
>>> # let's try it out!
>>> ops = NewOps()
>>> for item in ['this', 'is', 'that', 'and', 'that', 'is', 'this']:
...     ops.add_item(item)
...
>>> ops.show()
{'th': 4, 'is': 2, 'an': 1}
>>> # See that we specified _return_a_copy_of_the_class=False?
>>> # Now look at what happens if we try to use Ops, the original class, again. It behaves like NewOps.
>>> # That's usually not the behavior we want, so be careful!
>>> ops = Ops()
>>> for item in ['this', 'is', 'that', 'and', 'that', 'is', 'this']:
...     ops.add_item(item)
...
>>> ops.show()
{'th': 4, 'is': 2, 'an': 1}
>>>
>>>

SECOND EXAMPLE

Wrap a dict (or rather, the safer collections.UserDict), doing weird things to the input and output keys and values

>>> val_in_trans = lambda x: 'hello {}'.format(x)  # prepend "hello " to incoming values
>>> val_out_trans = lambda x: re.sub('hello', 'hi', x)  # replace "hello" by "hi" in output values
>>> key_in_trans = lambda x: '__' + x  # prepend incoming keys with double underscore
>>> key_out_trans = lambda x: x[2:]  # remove the first two characters (underscores) from keys when output
>>>
>>> methods_specs_dict = {
...     ('__contains__', '__getitem__', '__setitem__', '__delitem__'): dict(key=key_in_trans),
...     '__setitem__': dict(item=val_in_trans),
...     '__iter__': dict(method_output_trans=lambda x: map(key_out_trans, x)),
...     '__getitem__': dict(method_output_trans=val_out_trans)
... }
>>>
>>> methods_specs_dict = mk_method_trans_spec_from_methods_specs_dict(methods_specs_dict)
>>>
>>> @wrap_class_methods_input_and_output(**methods_specs_dict)
... class AA(UserDict):
...     pass
...
>>> aa = AA()
>>> aa['foo'] = 'shoo'  # store 'shoo' under 'foo'
>>> # the __str__ method isn't wrapped, so we see the actual STORED keys and values
>>> # we see that __foo, not foo is the actual key, and "hello shoo" the value:
>>> assert str(aa) == "{'__foo': 'hello shoo'}"
>>> assert 'foo' in aa  # yet from the interface, it looks like 'foo' is a key of aa...
>>> assert '__foo' not in aa  # ... and '__foo' is not a key.
>>> aa['foo'] = 'bar'  # let's replace the value of 'foo'
>>> assert str(aa) == "{'__foo': 'hello bar'}"  # see what's stored
>>> aa['star'] = 'wars'  # let's add another
>>> assert list(aa) == ['foo', 'star']  # what are the keys? (this uses __iter__ under the hood)
>>> # In the following, we'll use methods keys(), values(), and items(), none of which we wrapped.
>>> # And yet, they work as expected, since they pass on their work to methods we wrapped.
>>> assert list(aa.keys()) == ['foo', 'star']  # another way to get keys
>>> # see here that when we ask for values, we don't get what we asked to store, ...
>>> # ... nor what is actually stored, but something else
>>> assert list(aa.values()) == ['hi bar', 'hi wars']
>>> assert str(list(aa.items())) == "[('foo', 'hi bar'), ('star', 'hi wars')]"  # the keys and values we get from items()
>>> assert str(aa) == "{'__foo': 'hello bar', '__star': 'hello wars'}"  # what is actually stored
>>> del aa['foo']  # testing deletion of a key
>>> assert str(aa) == "{'__star': 'hello wars'}"  # it worked!
>>>
>>>

THIRD EXAMPLE

Here again, we’ll wrap UserDict. But instead of being silly, we’ll pretend we need to store waveforms in binary format (so input values will have to be wrapped), but still retrieving these waveforms as lists (so output values will have to be wrapped). Additionally, we’ll pretend we’re working with wav files within some root directory, but don’t want the root dir or the ‘.wav’ extension to appear in our keys. So we’ll have to wrap input and output keys. Of course, this is just pretend. Don’t use this with real waveforms. It won’t work.

>>> root = '/ROOT/DIR/'
>>> abs_path_of_rel_path = lambda rel_path: root + rel_path + '.wav'  # transform a relative path to an absolute one
>>> rel_path_of_abs_path = lambda x: x.replace(root, '').replace('.wav', '')  # transform an absolute path to a relative one
>>> list_to_bytes = bytes
>>> bytes_to_list = list
>>>
>>> methods_specs_dict = {
...     ('__contains__', '__getitem__', '__setitem__', '__delitem__'): dict(key=abs_path_of_rel_path),
...     '__setitem__': dict(item=list_to_bytes),
...     '__iter__': dict(method_output_trans=lambda x: map(rel_path_of_abs_path, x)),
...     '__getitem__': dict(method_output_trans=bytes_to_list)
... }
>>>
>>> methods_specs_dict = mk_method_trans_spec_from_methods_specs_dict(methods_specs_dict)
>>>
>>> @wrap_class_methods_input_and_output(**methods_specs_dict)
... class Wf(UserDict):
...     pass
...
>>> year = [2, 0, 1, 9]
>>> down = [5, 4, 3, 2, 1]
>>>
>>> wf = Wf()
>>> wf['year'] = year
>>> print(str(wf).replace("b'", "'"))
{'/ROOT/DIR/year.wav': '\x02\x00\x01\t'}
>>> 'year' in wf
True
>>> wf['down'] = down
>>> print(str(wf).replace("b'", "'"))
{'/ROOT/DIR/year.wav': '\x02\x00\x01\t', '/ROOT/DIR/down.wav': '\x05\x04\x03\x02\x01'}
>>> list(wf.keys())
['year', 'down']
>>> list(wf.values())
[[2, 0, 1, 9], [5, 4, 3, 2, 1]]
>>> list(wf.items())
[('year', [2, 0, 1, 9]), ('down', [5, 4, 3, 2, 1])]
>>> len(wf)
2
>>> del wf['year']
>>> len(wf)
1
>>> list(wf.items())
[('down', [5, 4, 3, 2, 1])]
i2.deco.wrap_instance_methods(_return_a_copy_of_the_class=True, _raise_error_if_non_existent_method=True, **method_trans_spec)[source]

Make a function that wraps the named methods of an instance, as wrap_class_methods_input_and_output does for a class (experimental).

_return_a_copy_of_the_class is accepted for symmetry but not used.

i2.deco.wrap_method_output(wrapper_func)[source]

Make a method decorator that applies wrapper_func to the method’s output.

i2.deco.wraps(wrapped, assigned=('__module__', '__name__', '__qualname__', '__doc__', '__annotations__', '__type_params__'), updated=('__dict__',))[source]

Copy of functools.wraps (kept local: it avoids a Jupyter tab-completion issue).