i2.wrapper

A wrapper object and tools to work with it

How the Wrap class works:

      *outer_args, **outer_kwargs
                 │
                 ▼
┌───────────────────────────────────┐
│              ingress              │
└───────────────────────────────────┘
                 │
                 ▼
      *inner_args, **inner_kwargs
                 │
                 ▼
┌───────────────────────────────────┐
│               func                │
└───────────────────────────────────┘
                 │
                 ▼
             func_output
                 │
                 ▼
┌───────────────────────────────────┐
│              egress               │
└───────────────────────────────────┘
                 │
                 ▼
            final_output

How the Ingress class (ingress templated function maker) works:

      *outer_args, **outer_kwargs
                 │
                 ▼
┌───────────────────────────────────┐
│          outer_sig_bind           │
└───────────────────────────────────┘
                 │
                 ▼
          outer_all_kwargs
                 │
                 ▼
┌───────────────────────────────────┐
│            kwargs_trans           │
└───────────────────────────────────┘
                 │
                 ▼
          inner_all_kwargs
                 │
                 ▼
┌───────────────────────────────────┐
│          inner_sig_bind           │
└───────────────────────────────────┘
                 │
                 ▼
      *inner_args, **inner_kwargs

Module Attributes

AUTO_PRESERVE_SIGNATURE

preserve_signature value meaning "decide per ingress" (see _should_preserve_signature()).

Functions

add_smart_defaults([func, ...])

Add smart defaults to function.

append_empty_args(func)

To use to transform an ingress function that only returns kwargs to one that returns the normal form of ingress functions: ((), kwargs)

apply_func_on_cond(func, cond, k, v)

Return func(v) if cond(k, v) is true, else v unchanged.

arg_val_converter(func, **conversion_for_arg)

Wrap func so that the given arguments are converted (name=converter) before the call.

arg_val_converter_ingress(func[, __strict])

Function form of ArgValConverterIngress: an ingress converting the named arguments.

bind_funcs_object_attrs(funcs[, ...])

Transform one or several functions into a class that contains them as methods sourcing specific arguments from the instance's attributes.

bind_funcs_object_attrs_old(funcs[, ...])

Transform one or several functions into a class that contains them as methods sourcing specific arguments from the instance's attributes.

camelize(s)

ch_names([func])

Change the argument names of a function.

complete_dict_applying_functions(d, /[, ...])

Complete dict d by applying function to variables in d, sequentially.

convert_VK_to_KO(kinds)

In a {name: kind} dict, replace VAR_KEYWORD kinds with KEYWORD_ONLY.

convert_dict_values(to_convert, ...)

Yield (key, value) pairs of to_convert, converting the values whose key has a function.

func_to_method_func(func[, instance_params, ...])

Get a 'method function' from a 'normal function'.

identity(x)

Return the input unchanged.

include_exclude([func, include, exclude])

Reorder and/or remove parameters.

include_exclude_ingress_factory(func[, ...])

A pattern underlying any ingress that takes a subset of parameters (possibly reordering them).

invert_map(d)

Swap keys and values of a mapping, raising ValueError if values are not unique.

items_with_mapped_keys(d, key_mapper)

Transform dict keys.

kwargs_trans([kwargs, _recursive, _inplace])

Transform a kwargs dict or build a transformer.

kwargs_trans_to_extract_args_from_attrs(...)

Pop the obj_param object out of outer_kwargs and source attr_names from its attributes.

map_names([func])

Change the argument names of a function.

mk_ingress_from_name_mapper(func, name_mapper, *)

Make an ingress that renames func's parameters ({inner_name: outer_name}).

modify_dict_on_cond(d, cond, func)

Copy d, applying func to the values whose (key, value) satisfy cond.

move_names_to_the_end(names, ...)

Remove the items of names_to_move_to_the_end from names and append to the right of names

move_params_to_the_end(func, names_to_move)

Choose args from func, according to choice_args_func and move them to the right

param_to_dataclass_field_tuple(param)

The (name, annotation, default) tuple dataclasses.make_dataclass expects for a field.

parameters_to_dict(parameters)

Map each parameter name to its parameter_to_dict (name, kind, default, annotation) dict.

params_used_in_funcs(funcs)

The set of parameter names of all the given functions.

partialx(func, *args[, __name__, ...])

Extends the functionality of builtin functools.partial with the ability to

required_params_used_in_funcs(funcs)

The set of names that are required (no default) in at least one of the given functions.

rm_params([func, params_to_remove, ...])

Get a function with some parameters removed.

transparent_egress(output)

transparent_ingress(*args, **kwargs)

wrap([func, ingress, egress, caller, name, ...])

Wrap a function, optionally transforming interface, input and output.

wrap_from_sig(func, new_sig)

Give func the signature new_sig, calling it with only the arguments it takes.

Classes

ArgNameMappingIngress(inner_sig, *[, ...])

Ingress that renames parameters: called with outer names, returns inner (args, kwargs).

ArgValConverterIngress(func[, ...])

Ingress with func's signature that applies name=converter functions to arguments.

Ingress(inner_sig[, kwargs_trans, ...])

The Ingress class offers a template for creating ingress classes.

InnerMapIngress(inner_sig[, kwargs_trans, ...])

Build an ingress from the wrapped function's signature and a spec of changes to it.

MakeFromFunc(func_to_obj)

Used to indicate that an object should be made as a function of an input func

PickleHelperMixin()

Mixin whose __reduce__ pickles an instance by its __name__ (a global reference).

SmartDefault(func_computing_default, ...)

Placeholder default for a parameter whose value add_smart_defaults computes from the other arguments.

Wrap(func[, ingress, egress, name, ...])

A function wrapper with interface modifiers.

Wrapx(func[, ingress, egress, caller, name])

An extended wrapping object that allows more complex wrapping mechanisms.

Exceptions

CallerValidationError

Raised when a caller is not valid

EgressValidationError

Raised when a egress is not valid

IngressValidationError

Raised when a ingress is not valid

WrapperValidationError

Raised when wrapper some construction params are not valid

i2.wrapper.AUTO_PRESERVE_SIGNATURE = 'auto'

preserve_signature value meaning “decide per ingress” (see _should_preserve_signature()). Named rather than spelled 'auto' at each use so the sentinel has exactly one definition.

class i2.wrapper.ArgNameMappingIngress(inner_sig, *, conserve_kind=False, **outer_name_for_inner_name)[source]

Bases: object

Ingress that renames parameters: called with outer names, returns inner (args, kwargs).

Unless conserve_kind=True, all parameter kinds of the outer signature become POSITIONAL_OR_KEYWORD. mk_ingress_from_name_mapper is the function form.

class i2.wrapper.ArgValConverterIngress(func, _ArgValConverterIngress__strict=True, **conversion_for_arg)[source]

Bases: object

Ingress with func’s signature that applies name=converter functions to arguments.

Names that are not parameters of func are rejected with an AssertionError at construction (the __strict parameter cannot be passed by keyword from outside the class, since the name is mangled; use arg_val_converter_ingress to switch it off).

exception i2.wrapper.CallerValidationError[source]

Bases: WrapperValidationError

Raised when a caller is not valid

exception i2.wrapper.EgressValidationError[source]

Bases: WrapperValidationError

Raised when a egress is not valid

class i2.wrapper.Ingress(inner_sig, kwargs_trans=None, outer_sig=None, *, allow_excess=True, apply_defaults=True, allow_partial=False)[source]

Bases: object

The Ingress class offers a template for creating ingress classes.

Note that when writing a decorator with i2.wrapper, you’re usually better off writing an ingress function for the purpose. As a result, your code will usually be less complex, easier to read, and more efficient than using the Ingress class.

So why use the Ingress class at all? For one, because it’ll take care of some common mechanics for you, so once you understand how to use it, you’ll probably create a correct wrapper faster.

Further, if you’re writing a general wrapping tool (e.g. your own currying machine, some rule-based input casting function, etc.) then you’ll find that using Ingres will usually with on the complexity, readability and/or efficiency front.

Consider the following function:

>>> def f(w, /, x: float, y=2, *, z: int = 3):
...     return f"(w:={w}) + (x:={x}) * (y:={y}) ** (z:={z}) == {w + x * y ** z}"
>>>
>>> f(0, 1)
'(w:=0) + (x:=1) * (y:=2) ** (z:=3) == 8'

Let’s say you wanted to dispatch this function to a command line interface, or a webservice where all arguments are taken from the url. The problem here is that this means that all incoming values will be strings in that case. Say you wanted all input values to be cast to ints. In that case you could do:

>>> from i2.wrapper import Ingress, wrap
>>> from inspect import signature
>>>
>>> trans_all_vals_to_ints = lambda d: {k: int(v) for k, v in d.items()}
>>>
>>> cli_f = wrap(
...     f,
...     ingress=Ingress(signature(f), kwargs_trans=trans_all_vals_to_ints)
... )
>>>
>>> cli_f("2", "3", "4")
'(w:=2) + (x:=3) * (y:=4) ** (z:=3) == 194'

In a more realistic situation, you’d want to have more control over this value transformation.

Say you wanted to convert to int if it’s possible, try float if not, and just leave the string alone otherwise.

>>> def _try_casting_to_numeric(x):
...     try:
...         return int(x)
...     except ValueError:
...         try:
...             return float(x)
...         except ValueError:
...             return x
...
>>> def cast_numbers(d: dict):
...     return {k: _try_casting_to_numeric(v) for k, v in d.items()}
>>>
>>> cli_f = wrap(f, ingress=Ingress(signature(f), kwargs_trans=cast_numbers))
>>>
>>> cli_f("2", "3.14", "4")
'(w:=2) + (x:=3.14) * (y:=4) ** (z:=3) == 202.96'

Let’s say that our values transformations are not all 1-to-1 as in the examples above. Instead, they can be

  • 1-to-many (e.g. the outer w is used to compute the inner w and x)

  • many-to-1 (e.g. the outer x and y are used to compute inner y)

  w   x   y   z
 / \   \ /    |
w   x   y     z
>>> def kwargs_trans(outer_kw):
...     return dict(
...         # e.g. 1-to-many: one outer arg (w) producing two inner args (w, and y)
...         w=outer_kw['w'] * 2,
...         x=outer_kw['w'] * 3,
...         # e.g. many-to-1: two outer args (x and y) producing one inner arg (y)
...         y=outer_kw['x'] + outer_kw['y'],
...         # Note that no z is mentioned: This means we're just leaving it alone
...     )
...
>>>
>>> ingress = Ingress(signature(f), kwargs_trans=kwargs_trans)
>>> assert ingress(2, x=3, y=4) == ((4,), {'x': 6, 'y': 7, 'z': 3})
>>>
>>> wrapped_f = wrap(f, ingress=ingress)
>>> assert wrapped_f(2, x=3, y=4) == '(w:=4) + (x:=6) * (y:=7) ** (z:=3) == 2062'

The following is an example that involves several aspects of the Ingress class.

>>> from i2 import Sig
>>> def kwargs_trans(outer_kw):
...     return dict(
...         w=outer_kw['w'] * 2,
...         x=outer_kw['w'] * 3,
...         # need to pop you (inner func has no you argument)
...         y=outer_kw['x'] + outer_kw.pop('you'),
...         # Note that no z is mentioned: This means we're just leaving it alone
...     )
>>>
>>> ingress = Ingress(
...     inner_sig=signature(f),
...     kwargs_trans=kwargs_trans,
...     outer_sig=Sig(f).ch_names(y='you')  # need to give the outer sig a you
...     # You could also express it this way (though you'd lose the annotations)
...     # outer_sig=lambda w, /, x, you=2, *, z=3: None
... )
>>> assert ingress(2, x=3, you=4) == ((4,), {'x': 6, 'y': 7, 'z': 3})
>>>
>>> wrapped_f = wrap(f, ingress=ingress)
>>> assert wrapped_f(2, x=3, you=4) == '(w:=4) + (x:=6) * (y:=7) ** (z:=3) == 2062'

A convenience method allows to do the same with the ingress instance itself:

>>> wrapped_f = ingress.wrap(f)
>>> assert wrapped_f(2, x=3, you=4) == '(w:=4) + (x:=6) * (y:=7) ** (z:=3) == 2062'
classmethod name_map(wrapped, **old_to_new_name)[source]

Change argument names.

>>> def f(w, /, x: float, y=2, *, z: int = 3):
...     return f"(w:={w}) + (x:={x}) * (y:={y}) ** (z:={z}) == {w + x * y ** z}"
>>> ingress = Ingress.name_map(f, w='DoubleYou', z='Zee')
>>> ingress
Ingress signature: (DoubleYou, /, x: float, y=2, *, Zee: int = 3)
>>> wrapped_f = ingress.wrap(f)
>>> wrapped_f(1, 2, y=3, Zee=4)
'(w:=1) + (x:=2) * (y:=3) ** (z:=4) == 163'
wrap(func, egress=None, *, name=None)[source]

Convenience method to wrap a function with the instance ingress. ingress.wrap(func,...) equivalent to Wrap(func, ingress, ...)

Return type:

Wrap

exception i2.wrapper.IngressValidationError[source]

Bases: WrapperValidationError

Raised when a ingress is not valid

class i2.wrapper.InnerMapIngress(inner_sig, kwargs_trans=None, *, _allow_reordering=False, **changes_for_name)[source]

Bases: object

Build an ingress from the wrapped function’s signature and a spec of changes to it.

Systematically, i.e. “according to a fixed plan/system” is what it’s about here. As we’ll see below, if you need to write a particular adapter for a specific case, you probably should do by writing an actual ingress function directly. In cases where you might want to apply a same logic to wrap many functions, you may want to fix that wrapping logic: InnerMapIngress provides one way to do this.

Parameters:
  • inner_sig – The signature of the wrapped function.

  • kwargs_trans (Callable[[dict], dict] | None) – A dict-to-dict transformation of the outer kwargs to the kwargs that should be input to the inner function. That is kwargs_trans is outer_kwargs -> inner_kwargs. Note that though both outer and inner signatures could have those annoying position-only kinds, you don’t have to think of that. The parameter kind restrictions are taken care of automatically.

  • _allow_reordering – Whether we want to allow reordering of variables

  • in_to_out_sig_changes – The inner_name=dict_of_changes_for_that_name pairs, the dict_of_changes_for_that_name is a dict with keys being valid inspect.Parameter

Consider the following function that has a position only, a keyword only, two arguments with annotations, and three with a default.

>>> def f(w, /, x: float = 1, y=2, *, z: int = 3):
...     return w + x * y ** z

Say we wanted a version of this function

  • that didn’t have the argument kind restrinctions (all POSITION_OR_KEYWORD),

  • where the annotation of x was changed int and the default removed

  • where y was named you instead, and has an annotation (int).

  • where the default of z was 10 instead of 3, and doesn’t have an annotation.

In order to get a version of this function we wanted (more lenient kinds, with some annotations and a default change), we can use the ingress function:

>>> def directly_defined_ingress(w, x: int, you: int=2, z = 10):
...     return (w,), dict(x=x, y=you, z=z)

When we need to wrap a specific function in a specific way, defining an ingress function this way is usually the simplest way. But in some cases we need to build the ingress function using some predefined rule/protocol to make applying the rule/protocol systematic.

For those cases, InnerMapIngress comes in handy.

With InnerMapIngress we’d build our ingress function like this:

>>> from inspect import Parameter, signature
>>> PK = Parameter.POSITIONAL_OR_KEYWORD
>>> empty = Parameter.empty
>>> ingress = InnerMapIngress(
...     f,
...     # change kind to PK:
...     w=dict(kind=PK),
...     # change annotation of x from float to int and remove default
...     x=dict(annotation=int, default=empty),
...     # rename y to you and add annotation int:
...     y=dict(name='you', annotation=int),
...     # change kind to PK, default to 10, and remove annotation:
...     z=dict(kind=PK, default=10, annotation=empty),
... )

Note

  • Only the changes we wish to make to the parameters are mentioned. You could also define the parameters explicitly by simply listing all three of the dimensions (kind, annotation, and default)

  • Three? But a Parameter object has four; what about the name? Indeed, you can use name as well, more on that later.

  • Note that in order to specify that you want no default, or no annotation, you cannot use None since None is both a valid default and a valid annotation; So instead you need to use Parameter.empty (conveniently assigned to a constant named empty in the wrapping module.

Now see that all arguments are POSITIONAL_OR_KEYWORD, x and y are int, and default of z is 10:

>>> assert (
...     str(signature(ingress))
...     == str(signature(directly_defined_ingress))
...     == '(w, x: int, you: int = 2, z=10)'
... )

Additionally, ingress function does it’s job of dispatching the right args and kwargs to the target function:

>>> assert (
...     ingress(0,1,2,3)
...     == directly_defined_ingress(0,1,2,3)
...     == ((0,), {'x': 1, 'y': 2, 'z': 3})
... )
classmethod from_signature(inner_sig, outer_sig, _allow_reordering=False)[source]

A convienience ingress constructor to specify wrappings that affect arguments independently.

Parameters:
  • inner_sig – The signature of wrapped, inner function (or the inner function itself)

  • outer_sig – The desired outer signature. Can also use a function (will only take it’s signature though).

  • _allow_reordering – Whether to allow outer_sig to reorder arguments.

Returns:

An ingress that will allow one to use a function having the inner_sig signature to

Say we wanted to get a version of the function:

>>> def f(w, /, x: float = 1, y=2, *, z: int = 3):
...     return w + x * y ** z

That was equivalent to (note the kind, default and annotation differences):

>>> def g(w, x=1, y: float = 2.0, z=10):
...     return w + x * y ** z
>>> h = wrap(f, ingress=InnerMapIngress.from_signature(f, g))
>>> Sig(h)
<Sig (w, x=1, y: float = 2.0, z=10)>

Note we could have used ...from_signature(Sig(f), Sig(g)) as well, since the method doesn’t use the actual functions, just their signatures.

So we’ve seen that h takes on the signature (kind, defaults, and annotations) of g. Let’s see now that h actually computes, uses the defaults of g and can doesn’t have the position only restriction on w.

>>> assert h(0) == g(0) == 1024 == 0 + 1 * 2 ** 10
>>> assert h(1,2) == g(1,2) == 2049 == 1 + 2 * 2 ** 10
>>> assert h(1,2,3,4) == g(1,2,3,4) == 1 + 2 * 3 ** 4
>>>
>>> assert h(w=1,x=2,y=3,z=4) == g(1,2,3,4) == 1 + 2 * 3 ** 4  # w keyword arg!
class i2.wrapper.MakeFromFunc(func_to_obj)[source]

Bases: object

Used to indicate that an object should be made as a function of an input func

class i2.wrapper.PickleHelperMixin[source]

Bases: object

Mixin whose __reduce__ pickles an instance by its __name__ (a global reference).

class i2.wrapper.SmartDefault(func_computing_default, original_default)[source]

Bases: object

Placeholder default for a parameter whose value add_smart_defaults computes from the other arguments.

Holds the function that computes the value and, if the parameter had one, its original default (shown in the repr).

original_default

alias of _empty

class i2.wrapper.Wrap(func, ingress=None, egress=None, *, name=None, preserve_signature='auto')[source]

Bases: _Wrap

A function wrapper with interface modifiers.

Parameters:
  • func – The wrapped function

  • ingress – The incoming data transformer. It determines the argument properties (name, kind, default and annotation) as well as the actual input of the wrapped function.

  • egress – The outgoing data transformer. It also takes precedence over the wrapped function to determine the return annotation of the Wrap instance

  • name – Name to give the wrapper (will use wrapped func name by default)

  • preserve_signature –

    Controls signature preservation from the wrapped function.

    • ’auto’ (default): Automatically preserve if ingress has (*args, **kwargs) signature

    • True: Always preserve signature from func (copies __signature__)

    • False: Don’t preserve (use ingress’s natural signature)

    When signature is preserved, both __signature__ and __annotations__ are copied from func to the wrapper, ensuring type checkers and IDEs see the original signature.

Returns:

A callable instance wrapping func

Some examples:

>>> from inspect import signature
>>> from i2 import Sig
>>> def func(a, b):
...     return a * b
>>> wrapped_func = wrap(func)  # no transformations: wrapped_func is the same as func
>>> assert wrapped_func(2, 'Hi') == func(2, 'Hi') == 'HiHi'

Modifying the first argument

>>> def ingress(a, b):
...   return (2 * a, b), dict()
>>> wrapped_func = wrap(func, ingress=ingress)  # first variable is now multiplied by 2
>>> wrapped_func(2, 'Hi')
'HiHiHiHi'

Same using keyword args, we need to use tuple to represent an empty tuple

>>> def ingress(a, b):
...   return tuple(), dict(a=2 * a, b=b) # Note that b MUST be present as well, or an error will be raised
>>> wrapped_func = wrap(func, ingress=ingress)  # first variable is now multiplied by 2
>>> wrapped_func(2, 'Hi')
'HiHiHiHi'

Using both args and kwargs

>>> def ingress(a, b):
...   return (2 * a, ), dict(b=b)
>>> wrapped_func = wrap(func, ingress=ingress)  # first variable is now multiplied by 2
>>> wrapped_func(2, 'Hi')
'HiHiHiHi'

We can use ingress to ADD parameters to func

>>> def ingress(a, b, c):
...   return (a, b + c), dict()
>>> wrapped_func = wrap(func, ingress=ingress)
>>> # now wrapped_func takes three arguments
>>> wrapped_func(2, 'Hi', 'world!')
'Hiworld!Hiworld!'

Egress is a bit more straightforward, it simply applies to the output of the wrapped function. We can use ingress to ADD parameters to func

>>> def egress(output):
...   return output + ' ITSME!!!'
>>> wrapped_func = wrap(func, ingress=ingress, egress=egress)
>>> # now wrapped_func takes three arguments
>>> wrapped_func(2, 'Hi', 'world!')
'Hiworld!Hiworld! ITSME!!!'

A more involved example:

>>> def ingress(a, b: str, c="hi"):
...     return (a + len(b) % 2,), dict(string=f"{c} {b}")
...
>>> def func(times, string):
...     return times * string
...
>>> wrapped_func = wrap(func, ingress=ingress)
>>> assert wrapped_func(2, "world! ", "Hi") == "Hi world! Hi world! Hi world! "
>>>
>>> wrapped_func = wrap(func, egress=len)
>>> assert wrapped_func(2, "co") == 4 == len("coco") == len(func(2, "co"))
>>>
>>> wrapped_func = wrap(func, ingress=ingress, egress=len)
>>> assert (
...     wrapped_func(2, "world! ", "Hi")
...     == 30
...     == len("Hi world! Hi world! Hi world! ")
... )

An ingress function links the interface of the wrapper to the interface of the wrapped func; therefore it’s definition often depends on information of both, and for that reason, we provide the ability to specify the ingress not only explicitly (as in the examples above), but through a factory – a function that will be called on func to produce the ingress that should be used to wrap it.

Common Patterns and Best Practices

Pattern 1: Transform inputs while preserving signature

By default (with preserve_signature=’auto’), Wrap automatically preserves signatures when your ingress uses (*args, **kwargs):

>>> def uppercase_args(func):
...     def ingress(*args, **kwargs):
...         args = tuple(str(a).upper() if isinstance(a, str) else a for a in args)
...         return args, kwargs
...     return Wrap(func, ingress=ingress)
>>>
>>> @uppercase_args
... def greet(name: str, greeting: str = "Hello") -> str:
...     return f"{greeting}, {name}!"
>>>
>>> greet("alice")  # Signature preserved, input transformed
'Hello, ALICE!'

Pattern 2: Keep return annotation with transparent egress

When using an egress that doesn’t transform the type, return annotations are automatically preserved:

>>> def add_logging(func):
...     def egress(output):
...         # print(f"Result: {output}")  # Commented out for doctest
...         return output  # Type unchanged
...     return Wrap(func, egress=egress)
>>>
>>> @add_logging
... def calculate(x: int) -> int:
...     return x * 2
>>>
>>> result = calculate(5)  # Return type preserved as int
>>> result
10

Pattern 3: Validation without transformation

Use ingress for validation without modifying arguments:

>>> def validate_positive(func):
...     def ingress(*args, **kwargs):
...         if any(a <= 0 for a in args if isinstance(a, (int, float))):
...             raise ValueError("All numeric arguments must be positive")
...         return args, kwargs
...     return Wrap(func, ingress=ingress)
>>>
>>> @validate_positive
... def multiply(x: int, y: int) -> int:
...     return x * y
>>>
>>> multiply(2, 3)
6

Pattern 4: Error handling and logging

Wrap both ends for comprehensive error handling:

>>> def safe_call(func):
...     def ingress(*args, **kwargs):
...         # print(f"Calling {func.__name__}")  # Commented out for doctest
...         return args, kwargs
...
...     def egress(output):
...         # print(f"Success: {output}")  # Commented out for doctest
...         return output
...
...     return Wrap(func, ingress=ingress, egress=egress)

Common Pitfalls and Solutions

Pitfall 1: Losing signatures with explicit non-generic ingress

If your ingress doesn’t use (*args, **kwargs), the auto mode won’t preserve the signature. Use preserve_signature=True explicitly:

>>> # WRONG: Signature lost with non-generic ingress
>>> def my_func(x: int, y: int = 5) -> int:
...     return x + y
>>>
>>> def ingress(x, y):  # Specific signature
...     return (x,), {'y': y}
>>>
>>> # Without explicit preservation, signature won't match original
>>> wrapped = Wrap(my_func, ingress=ingress, preserve_signature=False)
>>> # Signature is now (x, y) instead of (x: int, y: int = 5) -> int
>>>
>>> # RIGHT: Explicit preservation
>>> wrapped = Wrap(my_func, ingress=ingress, preserve_signature=True)
>>> # Now signature is correctly (x: int, y: int = 5) -> int

Pitfall 2: Type-changing egress without annotation

If your egress changes the output type, annotate it. Otherwise, the function’s original return type will be preserved, creating incorrect type hints:

>>> # RIGHT: Egress annotated with correct return type
>>> def stringify(func):
...     def egress(output) -> str:  # Annotated!
...         return str(output)
...     return Wrap(func, egress=egress)
>>>
>>> @stringify
... def calc(x: int) -> str:  # Note: return annotation updated
...     return x * 2
>>>
>>> isinstance(calc(5), str)
True

Pitfall 3: Forgetting to return (args, kwargs) from ingress

Ingress MUST return a tuple of (args, kwargs) for the wrapped function:

>>> # WRONG: ingress doesn't return (args, kwargs)
>>> # def broken_ingress(*args, **kwargs):
>>> #     print("called")
>>> #     return None  # WRONG! Must return (args, kwargs)
>>>
>>> # RIGHT: Always return (args, kwargs)
>>> def correct_ingress(*args, **kwargs):
...     # Do any processing here
...     return args, kwargs  # CORRECT

Pitfall 4: Modifying mutable arguments in place

Be careful when modifying arguments - changes affect the original objects:

>>> # RIGHT: Create new objects
>>> def fixed(func):
...     def ingress(*args, **kwargs):
...         if args and isinstance(args[0], list):
...             args = ([*args[0], 999],) + args[1:]  # New list
...         return args, kwargs
...     return Wrap(func, ingress=ingress)

Backward Compatibility Notes

Signature Preservation (v3.0):

Currently, preserve_signature defaults to ‘auto’ which only preserves signatures for generic (*args, **kwargs) ingress functions. In v3.0, we may change the default to True to always preserve signatures unless explicitly disabled. This matches user expectations that decorators should preserve signatures by default.

To prepare for this change:

  • If you want current behavior: explicitly set preserve_signature=’auto’

  • If you want v3.0 behavior: explicitly set preserve_signature=True

  • If you never want preservation: explicitly set preserve_signature=False

See also

wrap function.

exception i2.wrapper.WrapperValidationError[source]

Bases: ValueError

Raised when wrapper some construction params are not valid

class i2.wrapper.Wrapx(func, ingress=None, egress=None, *, caller=None, name=None)[source]

Bases: _Wrap

An extended wrapping object that allows more complex wrapping mechanisms.

Parameters:
  • func – The wrapped function

  • ingress – The incoming data transformer. It determines the argument properties (name, kind, default and annotation) as well as the actual input of the wrapped function.

  • egress – The outgoing data transformer. It also takes precedence over the wrapped function to determine the return annotation of the Wrap instance

  • caller – A caller defines what it means to call the func on the arguments it is given. It should be of the form caller(func, args, kwargs, *, ...extra_keyword_only_params). By default, the caller will simply return func(*args, **kwargs).

  • name – Name to give the wrapper (will use wrapped func name by default)

Returns:

A callable instance wrapping func

>>> from inspect import signature
>>>
>>> def func(x, y):
...     return x + y
...
>>> def save_on_output_egress(v, *, k, s):
...     s[k] = v
...     return v
...
>>> save_on_output = Wrapx(func, egress=save_on_output_egress)
>>> # TODO: should be `(x, y, *, k, s)` --> Need to work on the merge for this.
>>> str(signature(save_on_output))
'(x, y, k, s)'
>>>
>>> store = dict()
>>> save_on_output(1, 2, k='save_here', s=store)
3
>>> assert save_on_output(1, 2, k='save_here', s=store) == 3 == func(1, 2)
>>> store  # see what's in the store now!
{'save_here': 3}

A caller is meant to control the way the function is called. It is given the func and the func_args and func_kwargs (whatever the ingress function gives it, if present) and possibly additional params and will return… well, what ever you tell it to.

This can be used, for example, to call the function in a subprocess, or on a remote system, differ computation (command pattern, for example, using functools.partial, or do what ever needs to have a view both on the function and its inputs.

Here, we will wrap the function so it will apply to an iterable of inputs (of the first argument), returning a list of results

>>> def func(x, y=2):
...     return x + y
...
>>> def iterize(func, args, kwargs):
...     first_arg_val = next(iter(kwargs.values()))
...     return list(map(func, first_arg_val))
...
>>> iterized_func = Wrapx(func, caller=iterize)
>>> iterized_func([1, 2, 3, 4])
[3, 4, 5, 6]

Let’s do the same as above, but allow other variables (here y) to be input as well. This takes a bit more work…

>>> from functools import partial
>>> def _iterize_first_arg(func, args, kwargs):
...     first_arg_name = next(iter(kwargs))
...     remaining_kwargs = {
...         k: v for k, v in kwargs.items() if k != first_arg_name
...     }
...     return list(
...         map(partial(func, **remaining_kwargs), kwargs[first_arg_name])
...     )

Let’s demo a different way of using Wrapx: Making a wrapper to apply at function definition time

>>> iterize_first_arg = partial(Wrapx, caller=_iterize_first_arg)
>>> @iterize_first_arg
... def func(x, y):
...     return x + y
>>>
>>> func([1, 2, 3, 4], 10)
[11, 12, 13, 14]
i2.wrapper.append_empty_args(func)[source]

To use to transform an ingress function that only returns kwargs to one that returns the normal form of ingress functions: ((), kwargs)

i2.wrapper.apply_func_on_cond(func, cond, k, v)[source]

Return func(v) if cond(k, v) is true, else v unchanged.

i2.wrapper.arg_val_converter(func, **conversion_for_arg)[source]

Wrap func so that the given arguments are converted (name=converter) before the call.

>>> def f(x, y=1):
...     return x + y
>>> g = arg_val_converter(f, x=int)
>>> g('2', 3)
5
i2.wrapper.arg_val_converter_ingress(func, __strict=True, **conversion_for_arg)[source]

Function form of ArgValConverterIngress: an ingress converting the named arguments.

i2.wrapper.bind_funcs_object_attrs(funcs, init_params=(), *, cls=None, module=None, **extra_attrs)[source]

Transform one or several functions into a class that contains them as methods sourcing specific arguments from the instance’s attributes.

>>> from inspect import signature
>>> from dataclasses import dataclass
>>>
>>> def foo(a, b, c=2, *, d='bar'):
...     return f"{d}: {(a + b) * c}"
>>> foo(1, 2)
'bar: 6'
>>> Klass = bind_funcs_object_attrs(foo, init_params='a c')
>>> Klass.__name__
'Foo'
>>> instance = Klass(a=1, c=3)
>>> assert instance.foo(2, d='hello') == 'hello: 9' == foo(
...     a=1, b=2, c=3, d='hello')
>>> str(signature(Klass))
'(a, c=2) -> None'
>>>
>>> instance = Klass(a=1, c=3)
>>> str(instance)
'Foo(a=1, c=3)'
>>> str(signature(instance.foo))
"(b, *, d='bar')"
>>> instance.foo(2, d='hello')
'hello: 9'
>>> instance.foo(10, d='goodbye')
'goodbye: 33'
>>> def foo(a, b, c):
...     return a + b * c
...
>>> def bar(d, e):
...     return f"{d=}, {e=}"
...
>>> @dataclass
... class K:
...     a: int
...     e: int
...
>>> C = bind_funcs_object_attrs([foo, bar], 'a e', cls=K)
>>> str(signature(C))
'(a: int, e: int) -> None'
>>> c = C(1,2)
>>> assert str(signature(c.foo)) == '(b, c)'
>>> c.foo(3,4)
13
>>> assert str(signature(c.bar)) == '(d)'
>>> c.bar(5)
'd=5, e=2'
i2.wrapper.bind_funcs_object_attrs_old(funcs, init_params=(), *, cls=None)[source]

Transform one or several functions into a class that contains them as methods sourcing specific arguments from the instance’s attributes.

>>> from inspect import signature
>>> from dataclasses import dataclass
>>>
>>> def foo(a, b, c=2, *, d='bar'):
...     return f"{d}: {(a + b) * c}"
>>> foo(1, 2)
'bar: 6'
>>> Klass = bind_funcs_object_attrs_old(foo, init_params='a c')
>>> Klass.__name__
'Foo'
>>> instance = Klass(a=1, c=3)
>>> assert instance.foo(2, d='hello') == 'hello: 9' == foo(
...     a=1, b=2, c=3, d='hello')
>>> str(signature(Klass))
'(a, c=2) -> None'
>>>
>>> instance = Klass(a=1, c=3)
>>> str(instance)
'Foo(a=1, c=3)'
>>> str(signature(instance.foo))
"(b, *, d='bar')"
>>> instance.foo(2, d='hello')
'hello: 9'
>>> instance.foo(10, d='goodbye')
'goodbye: 33'
>>> def foo(a, b, c):
...     return a + b * c
...
>>> def bar(d, e):
...     return f"{d=}, {e=}"
...
>>> @dataclass
... class K:
...     a: int
...     e: int
...
>>> C = bind_funcs_object_attrs([foo, bar], 'a e', cls=K)
>>> str(signature(C))
'(a: int, e: int) -> None'
>>> c = C(1,2)
>>> assert str(signature(c.foo)) == '(b, c)'
>>> c.foo(3,4)
13
>>> assert str(signature(c.bar)) == '(d)'
>>> c.bar(5)
'd=5, e=2'
i2.wrapper.camelize(s)[source]
>>> camelize('camel_case')
'CamelCase'
i2.wrapper.complete_dict_applying_functions(d, /, _only_if_name_missing=True, _allow_overwrites=False, **func_for_name)[source]

Complete dict d by applying function to variables in d, sequentially.

That is, doing d[name] = func(**d) for all name, func in d.items().

Set _allow_overwrites=True to allow overwrites.

Set _only_if_name_missing=False to apply all functions of func_for_name regardless if the name already exists in d or not.

>>> func_for_name = dict(
...     b=lambda a: a * 10, c=lambda a, b: a + b, d=lambda c: c * 2
... )
>>> complete_dict_applying_functions(dict(a=1), **func_for_name)
{'a': 1, 'b': 10, 'c': 11, 'd': 22}

Notice that when b is present in input dict, it’s value is conserved. That is, the b of func_for_name isn’t applied to compute it.

>>> complete_dict_applying_functions(dict(a=1, b=2), **func_for_name)
{'a': 1, 'b': 2, 'c': 3, 'd': 6}

If you specify _only_if_name_missing=False, complete_dict_applying_functions will try to compute everything func_for_name tells it too, regardless if the input dictionary contains the key or not, resulting in an error:

>>> complete_dict_applying_functions(
...     dict(a=1, b=2), **func_for_name, _only_if_name_missing=False
... )
Traceback (most recent call last):
  ...
i2.errors.OverwritesNotAllowed: You're not allowed to overwrite to the values of b

If you want, on the other hand, to allow overwrites, you can do so specifying _allow_overwrites=True:

>>> complete_dict_applying_functions(
...     dict(a=1, b=2), **func_for_name,
...     _only_if_name_missing=False, _allow_overwrites=True
... )
{'a': 1, 'b': 10, 'c': 11, 'd': 22}
i2.wrapper.convert_VK_to_KO(kinds)[source]

In a {name: kind} dict, replace VAR_KEYWORD kinds with KEYWORD_ONLY.

i2.wrapper.convert_dict_values(to_convert, key_to_conversion_function)[source]

Yield (key, value) pairs of to_convert, converting the values whose key has a function.

>>> dict(convert_dict_values({'x': '2', 'y': 3}, {'x': int}))
{'x': 2, 'y': 3}
i2.wrapper.func_to_method_func(func, instance_params=(), *, method_name=None, method_params=None, instance_arg_name='self')[source]

Get a ‘method function’ from a ‘normal function’. Also known as “methodize”.

That is, get a function that gives the same outputs as the ‘normal function’, except that some of the arguments are sourced from the attributes of the first argument.

The intended use case is when you want to inject one or several methods in a class or instance, sourcing some of the arguments of the underlying function from a common pool: The attributes of the instance.

Consider the following function involving four parameters: a, b, c and d.

Return type:

Callable

>>> def func(a, b: int, c=2, *, d='bar'):
...     return f"{d}: {(a + b) * c}"
>>> func(1, 2, c=3, d='hello')
'hello: 9'

If we wanted to make an equivalent “method function” that would source it’s a and it’s c from the first argument’s (in practice this first argument will be and instance of the class the method will be bound to), we can do so like so:

>>> method_func = func_to_method_func(func, 'a c')
>>> from inspect import signature
>>> str(signature(method_func))
"(self, b: int, *, d='bar')"

Note that the first argument is self (default name for an “instance”), that a and c are not there, but that the two remaining parameters, b and d are present, in the same order, and with the same annotations and parameter kind (the d is still keyword-only).

Now let’s make a dummy object that has attributes a and a c, and use it to call method_func:

>>> from collections import namedtuple
>>> instance = namedtuple('FakeInstance', 'a c')(1, 3)
>>> method_func(instance, 2, d='hello')
'hello: 9'

Which is:

>>> assert method_func(instance, 2, d='hello') == func(1, 2, c=3, d='hello')

Often, though, what you’ll want is to include this method function directly in a class, as you’re making that class “normally”. That works too:

>>> from dataclasses import dataclass
>>> @dataclass
... class Klass:
...     a : int = 1
...     c : int = 3
...     method_func = func_to_method_func(func, 'a c')
>>> instance = Klass(1, 3)
>>> instance.method_func(2, d='hello')
'hello: 9'

What if your function has argument names that don’t correspond to the names you have, or want, as attributes of the class? Or even, you have several functions that share an argument name that need to be bound to a different attribute?

For that, just use map_names to wrap the function, giving it the names that you need to give it to have the effect you want (the binding of those arguments to attributes of the instance):

>>> from i2.wrapper import ch_names
>>> def func(x, y: int, z=2, *, d='bar'):
...     return f"{d}: {(x + y) * z}"
>>> from dataclasses import dataclass
>>> @dataclass
... class Klass:
...     a : int = 1
...     c : int = 3
...     method_func = func_to_method_func(ch_names(func, x='a', z='c'), 'a c')
>>> instance = Klass(1, 3)
>>> instance.method_func(2, d='hello')
'hello: 9'
i2.wrapper.identity(x)[source]

Return the input unchanged.

>>> identity('x')
'x'
i2.wrapper.include_exclude_ingress_factory(func, include=None, exclude=None, *, allow_partial=False)[source]

A pattern underlying any ingress that takes a subset of parameters (possibly reordering them).

For example: Keep only required arguments, or reorder params to be able to partialize #3 (without having to partialize #1 and #2)

i2.wrapper.invert_map(d)[source]

Swap keys and values of a mapping, raising ValueError if values are not unique.

>>> invert_map({'a': 1, 'b': 2})
{1: 'a', 2: 'b'}
i2.wrapper.items_with_mapped_keys(d, key_mapper)[source]

Transform dict keys. More precisely yield (new_k,v) pairs from a key mapper dict.

Parameters:
  • d (dict) – src dict

  • key_mapper – {old_name: new_name, …} mapping

Returns:

generator of (new_name, value) pairs

Often used in conjunction with dict:

>>> dict(items_with_mapped_keys(
...     {'a': 1, 'b': 2, 'c': 3, 'd': 4},
...     {'a': 'Ay', 'd': 'Dee'})
... )
{'Ay': 1, 'b': 2, 'c': 3, 'Dee': 4}
i2.wrapper.kwargs_trans(kwargs=None, /, _recursive=False, _inplace=False, **key_and_val_func)[source]

Transform a kwargs dict or build a transformer.

Parameters:
  • kwargs (dict) – The dict containing the input kwargs that we will transform

  • _recursive – Whether the transformations listed in key_and_val_func should be applied “recursively”. When set to False (default), each transformation function applies to the original kwargs, not the one that was transformed, so far.

  • _inplace – If set to False will make a shallow copy of the kwargs before transforming it (only relevant if _recursive=True

  • key_and_val_func – The key=val_func pairs that indicate that a val_func should be applied to the kwargs, maching the argument names of the val_func to the keys of kwargs and extracting the values found therein to use for the corresponding inputs of that val_func.

Returns:

The transformed kwargs.

>>> d = dict(a=1, b=2, c=3)
>>> kwargs_trans(
...     d,
...     a=lambda a: a * 10,
...     b=lambda a, b: a + b
... )
{'a': 10, 'b': 3, 'c': 3}

See that d is unchanged here (transformation is not in place).

>>> d
{'a': 1, 'b': 2, 'c': 3}

Typically you’ll use kwargs_trans as a factory:

>>> trans = kwargs_trans(a=lambda a: a * 10, b=lambda a, b: a + b)
>>> trans(d)
{'a': 10, 'b': 3, 'c': 3}

Here we’ll demo what the _recursive and _inplace arguments do.

>>> from functools import partial
>>> re_kwargs_trans = partial(kwargs_trans, _recursive=True, _inplace=True)
>>> d = dict(a=1, b=2, c=3)
>>>
>>> re_kwargs_trans(
...     d,
...     a=lambda a: a * 10,
...     b=lambda a, b: a + b
...     # since _recursive=True, the a that is used is the new a = 10, not a = 1:
... )
{'a': 10, 'b': 12, 'c': 3}

Since _inplace=True, d itself has changed:

>>> d
{'a': 10, 'b': 12, 'c': 3}

Sometimes you’ll pipe several transformers together:

>>> from i2 import Pipe
>>> trans = Pipe(
...     re_kwargs_trans(a=lambda a: a / 10),
...     re_kwargs_trans(c=lambda a,b,c: a * b * c),
...     # and then compute a new value of a using a and c:
...     re_kwargs_trans(a=lambda a, c: c - 1),
... )
>>> trans(d)
{'a': 35.0, 'b': 12, 'c': 36.0}
i2.wrapper.kwargs_trans_to_extract_args_from_attrs(outer_kwargs, attr_names=(), obj_param='self')[source]

Pop the obj_param object out of outer_kwargs and source attr_names from its attributes.

Mutates outer_kwargs (the object is popped). Explicit kwargs win over attributes.

>>> class O: pass
>>> o = O(); o.a = 1; o.b = 2
>>> kwargs_trans_to_extract_args_from_attrs({'self': o, 'c': 3}, attr_names=('a', 'b'))
{'a': 1, 'b': 2, 'c': 3}
i2.wrapper.mk_ingress_from_name_mapper(func, name_mapper, *, conserve_kind=False)[source]

Make an ingress that renames func’s parameters ({inner_name: outer_name}).

>>> def foo(a, b: int, c=7):
...     return (a, b, c)
>>> ingress = mk_ingress_from_name_mapper(foo, dict(a='aa', c='cc'))
>>> Sig(ingress)
<Sig (aa, b: int, cc=7)>
>>> ingress(1, b=2, cc=3)
((), {'a': 1, 'b': 2, 'c': 3})
>>> wrap(foo, ingress=ingress)(1, 2, cc=3)
(1, 2, 3)

By default the outer signature loses positional-only and keyword-only kinds; conserve_kind=True keeps them.

i2.wrapper.modify_dict_on_cond(d, cond, func)[source]

Copy d, applying func to the values whose (key, value) satisfy cond.

i2.wrapper.move_names_to_the_end(names, names_to_move_to_the_end)[source]

Remove the items of names_to_move_to_the_end from names and append to the right of names

>>> names = ['a','c','d','e']
>>> names_to_move_to_the_end = ['c','e']
>>> move_names_to_the_end(names, names_to_move_to_the_end)
['a', 'd', 'c', 'e']
>>> names_to_move_to_the_end = 'c e'
>>> move_names_to_the_end(names, names_to_move_to_the_end)
['a', 'd', 'c', 'e']
i2.wrapper.move_params_to_the_end(func, names_to_move)[source]

Choose args from func, according to choice_args_func and move them to the right

>>> from functools import partial
>>> from i2 import Sig
>>> def foo(a, b, c):
...     return a + b + c
>>> g = partial(foo, b=4)  # fixing a, which is before b
>>> h = move_params_to_the_end(g, Sig(g).defaults)
>>> assert str(Sig(g)) == '(a, *, b=4, c)'
>>> assert str(Sig(h)) == '(a, *, c, b=4)'
i2.wrapper.param_to_dataclass_field_tuple(param)[source]

The (name, annotation, default) tuple dataclasses.make_dataclass expects for a field.

i2.wrapper.parameters_to_dict(parameters)[source]

Map each parameter name to its parameter_to_dict (name, kind, default, annotation) dict.

i2.wrapper.params_used_in_funcs(funcs)[source]

The set of parameter names of all the given functions.

i2.wrapper.partialx(func, *args, __name__=None, _rm_partialize=False, _allow_reordering=False, **kwargs)[source]

Extends the functionality of builtin functools.partial with the ability to

  • set __name__

  • remove partialized arguments from signature

  • reorder params (so that defaults are at the end)

>>> def f(a, b=2, c=3):
...     return a + b * c
>>> curried_f = partialx(f, c=10, _rm_partialize=True)
>>> curried_f.__name__
'f'
>>> from inspect import signature
>>> str(signature(curried_f))
'(a, b=2)'
>>> def f(a, b, c=3):
...     return a + b * c

Note that a gets a default, but b does not, yet is after a. This is allowed because these parameters all became KEYWORD_ONLY.

>>> g = partialx(f, a=1)
>>> str(Sig(g))
'(*, a=1, b, c=3)'

If you wanted to reorder the parameters to have all defaulted kinds be at the end, as usual, you can do so using _allow_reordering=True

>>> g = partialx(f, a=1, _allow_reordering=True)
>>> str(Sig(g))
'(*, b, a=1, c=3)'
i2.wrapper.required_params_used_in_funcs(funcs)[source]

The set of names that are required (no default) in at least one of the given functions.

i2.wrapper.transparent_egress(output)[source]
>>> transparent_egress('unnecessary_doctest')
'unnecessary_doctest'
i2.wrapper.transparent_ingress(*args, **kwargs)[source]
>>> transparent_ingress(1, 2, test=1)
((1, 2), {'test': 1})
i2.wrapper.wrap_from_sig(func, new_sig)[source]

Give func the signature new_sig, calling it with only the arguments it takes.

>>> def f(x, y=1):
...     return x + y
>>> h = wrap_from_sig(f, Sig('(x, y=1, z=0)'))
>>> Sig(h)
<Sig (x, y=1, z=0)>
>>> h(1, 2, 3), h(1, z=5)
(3, 2)