# i2.deco

Decorator tools

### Functions

| [`add_method`](#i2.deco.add_method)(obj, method_func[, method_name, ...])   | Dynamically add a method to an object.                                                                                                       |
|-----------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------|
| [`assert_attrs`](#i2.deco.assert_attrs)(attrs)                                | Asserts, at construction time, that the class contains a specific set of attributes                                                          |
| [`double_up_as_factory`](#i2.deco.double_up_as_factory)(decorator_func)               | Repurpose a decorator both as it's original form, and as a decorator factory.                                                                |
| [`ensure_iterable_args`](#i2.deco.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`](#i2.deco.get_callable_from_factory_if_no_arguments)(...)     | Will return the input itself if it's a callable with at least one argument.                                                                  |
| [`identity`](#i2.deco.identity)(obj)                                      | Return the input unchanged.                                                                                                                  |
| [`input_output_decorator`](#i2.deco.input_output_decorator)([preprocess, postprocess])  | Makes a decorator that preprocesses inputs and postprocesses outputs.                                                                        |
| [`is_not_set`](#i2.deco.is_not_set)(x)                                      | Return `True` if `x` is the `NotSet` sentinel, and `False` otherwise.                                                                        |
| [`kwargs_for_func`](#i2.deco.kwargs_for_func)(\*funcs, \*\*kwargs)               |                                                                                                                                              |
| [`mk_args_kwargs_merger`](#i2.deco.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`](#i2.deco.mk_call_logger)([logger, what_to_log, ...])         | Makes a decorator that logs each call to the wrapped function.                                                                               |
| [`mk_input_and_output_method_wrapper`](#i2.deco.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`](#i2.deco.mk_method_trans_spec_from_methods_specs_dict)(...)  | Utility to make inputs for wrap_class_methods_input_and_output more easily.                                                                  |
| [`postprocess`](#i2.deco.postprocess)(post[, caught_post_errors, ...])       | Add some post-processing after a function                                                                                                    |
| [`preprocess`](#i2.deco.preprocess)(pre)                                    | Make a decorator that feeds the wrapped function the output of `pre`.                                                                        |
| [`preprocess_arguments`](#i2.deco.preprocess_arguments)(pre)                          | Make a decorator that lets `pre` rewrite the `(args, kwargs)` of every call.                                                                 |
| [`transform_args`](#i2.deco.transform_args)([dflt_trans_func])                  | Make a decorator that transforms function arguments before calling the function.                                                             |
| [`transform_class_method_input_and_output`](#i2.deco.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`](#i2.deco.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`](#i2.deco.transparently_wrapped)(func)                        | Wrap `func` so it is called with its positional arguments packed in one tuple.                                                               |
| [`wrap_class_methods`](#i2.deco.wrap_class_methods)([...])                          | Make a decorator that wraps specific methods.                                                                                                |
| [`wrap_class_methods_input_and_output`](#i2.deco.wrap_class_methods_input_and_output)([...])         | Make a decorator that wraps specific methods, transforming specific argument values a nd output values.                                      |
| [`wrap_instance_methods`](#i2.deco.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`](#i2.deco.wrap_method_output)(wrapper_func)                   | Make a method decorator that applies `wrapper_func` to the method's output.                                                                  |
| [`wraps`](#i2.deco.wraps)(wrapped[, assigned, updated])                | Copy of `functools.wraps` (kept local: it avoids a Jupyter tab-completion issue).                                                            |

### Classes

| [`FuncFactory`](#i2.deco.FuncFactory)(func, \*[, include, exclude])   | Make a function factory.   |
|----------------------------------------------------------------------------------------------|----------------------------|

### Exceptions

| [`OutputPostProcessingError`](#i2.deco.OutputPostProcessingError)   | Raised by `postprocess` when the post-processing function fails.   |
|------------------------------------------------------------------------------|--------------------------------------------------------------------|

### *class* i2.deco.FuncFactory(func, , include=(), exclude=())

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#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.

```pycon
>>> 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:

```text
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:

```pycon
>>> 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`:

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

Note that:

```pycon
>>> 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.

```pycon
>>> 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:

```pycon
>>> 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:

```pycon
>>> get_42, get_hello = map(FuncFactory.func_returning_obj, (42, 'hello'))
>>> get_42()
42
>>> get_hello()
'hello'
```

#### to_jdict()

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

#### *classmethod* wrap(include=(), exclude=())

Return a `FuncFactory` constructor with `include` and `exclude` fixed.

### *exception* i2.deco.OutputPostProcessingError

Bases: [`RuntimeError`](https://docs.python.org/3/builtins/exceptions.html#RuntimeError)

Raised by `postprocess` when the post-processing function fails.

### i2.deco.add_method(obj, method_func, method_name=None, class_name=None)

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)

```pycon
>>> 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)

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

```pycon
>>> @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)

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.

```pycon
>>> @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:

```pycon
>>> 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:

```pycon
>>> from functools import partial
>>> isinstance(decorator(multiplier=3), partial)
True
>>> isinstance(decorator(func=foo), partial)
False
```

```pycon
>>> 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).

```pycon
>>> @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`:

```pycon
>>> @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:

```pycon
>>> 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:

```pycon
>>> 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)

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.

```pycon
>>> @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:

```pycon
>>> greet_people = ensure_iterable_args(greet_people, names=str)
```

### i2.deco.get_callable_from_factory_if_no_arguments(func_or_factory_thereof)

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)

Return the input unchanged.

* **Return type:**
  [`TypeVar`](https://docs.python.org/3/library/typing.html#typing.TypeVar)(`T`)

### i2.deco.input_output_decorator(preprocess=None, postprocess=None)

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!

```pycon
>>> # 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)

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`](https://docs.python.org/3/builtins/functions.html#bool)

```pycon
>>> 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)

* **Parameters:**
  * **funcs**
  * **kwargs**
* **Returns:**

```pycon
>>> 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)

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

```pycon
>>> 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)

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`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable)[[[`Callable`](https://docs.python.org/3/library/collections.abc.html#collections.abc.Callable), [`tuple`](https://docs.python.org/3/builtins/stdtypes.html#tuple), [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)], [`Any`](https://docs.python.org/3/library/typing.html#typing.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

```pycon
>>> # 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

```pycon
>>> @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…

```pycon
>>>
>>> # 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)

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)

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.

```pycon
>>> 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)

Add some post-processing after a function

* **Parameters:**
  **post** – The function to apply to the output

```pycon
>>> 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.

```pycon
>>> 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

```pycon
>>> 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)

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.

```pycon
>>> @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)

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.

```pycon
>>> @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)

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

```pycon
>>> # 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)

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)

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

### i2.deco.transparently_wrapped(func)

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)

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.

```pycon
>>> 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)

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.

```pycon
>>> 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

```pycon
>>> 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.

```pycon
>>> 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)

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)

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_\_',))

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