i2.deco¶
Decorator tools
Functions
|
Dynamically add a method to an object. |
|
Asserts, at construction time, that the class contains a specific set of attributes |
|
Repurpose a decorator both as it's original form, and as a decorator factory. |
|
Wrap a function so that specific arguments are assured to be iterable if they meet specific conditions. |
Will return the input itself if it's a callable with at least one argument. |
|
|
Return the input unchanged. |
|
Makes a decorator that preprocesses inputs and postprocesses outputs. |
|
Return |
|
|
|
Make a function that will return a dict containing all {argname: argval} pairs from a function's call. |
|
Makes a decorator that logs each call to the wrapped function. |
Make a method decorator transforming named arguments ( |
|
Utility to make inputs for wrap_class_methods_input_and_output more easily. |
|
|
Add some post-processing after a function |
|
Make a decorator that feeds the wrapped function the output of |
|
Make a decorator that lets |
|
Make a decorator that transforms function arguments before calling the function. |
|
Replace |
Instance-level counterpart of |
|
|
Wrap |
|
Make a decorator that wraps specific methods. |
Make a decorator that wraps specific methods, transforming specific argument values a nd output values. |
|
|
Make a function that wraps the named methods of an instance, as |
|
Make a method decorator that applies |
|
Copy of |
Classes
|
Make a function factory. |
Exceptions
Raised by |
- class i2.deco.FuncFactory(func, *, include=(), exclude=())[source]¶
Bases:
objectMake a function factory.
but more convenient and helpful (e.g. is picklable, produces functions with signatures, etc.)
One can use
functools.partialsto fix, or change, the defaults of arguments of a functionfuncthereby 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) 23In essence,
FuncFactoryis 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
FuncFactoryis a factory of functions, that is, it can make functions for you based on the instance’s underlyingfunc:>>> f = factory(b=2, c=3) >>> f(10) 23Note that:
>>> ff = factory(2, 3) # equivalent to ``factory(a=2, b=3)`` >>> ff(c=10) 16Further, you can tell
FuncFactorytoincludeorexcludespecific 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) 23Recipe: 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: objevery 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() 42Note
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'
- exception i2.deco.OutputPostProcessingError[source]¶
Bases:
RuntimeErrorRaised by
postprocesswhen 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 dowrapped_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) 30The 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**kwargsis a very common way to call a decorator, sodecorator(func=foo)must mean whatdecorator(foo)means:>>> decorator(func=foo, multiplier=10)(2) 30 >>> decorator(func=foo)(2) 6It 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) 9Note 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-onlyNote 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 isfuncand 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 “renamefunctocallback”, 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 usedouble_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), butensure_iterable_argsallows 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.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
Trueifxis theNotSetsentinel, andFalseotherwise.Signature consumers (UI or schema generators, for example) can use it to treat a
NotSetdefault likeinspect.Parameter.empty, i.e. “required, no default”:- Return type:
>>> 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') -> hahahahaAnd 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) 6Note
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") 42See also
postprocess: apply a function to the output instead.preprocess_arguments:prereturns 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
prerewrite 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") 3See also
preprocess:prereturns 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.methodin place with a version whose named arguments are transformed byarg_transand whose output bymethod_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
funcso 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_outputdoes for a class (experimental)._return_a_copy_of_the_classis accepted for symmetry but not used.