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
|
Functions
|
Add smart defaults to function. |
|
To use to transform an ingress function that only returns kwargs to one that returns the normal form of ingress functions: ((), kwargs) |
|
Return |
|
Wrap |
|
Function form of |
|
Transform one or several functions into a class that contains them as methods sourcing specific arguments from the instance's attributes. |
|
Transform one or several functions into a class that contains them as methods sourcing specific arguments from the instance's attributes. |
|
|
|
Change the argument names of a function. |
|
Complete dict |
|
In a |
|
Yield |
|
Get a 'method function' from a 'normal function'. |
|
Return the input unchanged. |
|
Reorder and/or remove parameters. |
|
A pattern underlying any ingress that takes a subset of parameters (possibly reordering them). |
|
Swap keys and values of a mapping, raising |
|
Transform dict keys. |
|
Transform a kwargs dict or build a transformer. |
Pop the |
|
|
Change the argument names of a function. |
|
Make an ingress that renames |
|
Copy |
|
Remove the items of |
|
Choose args from func, according to choice_args_func and move them to the right |
The |
|
|
Map each parameter name to its |
|
The set of parameter names of all the given functions. |
|
Extends the functionality of builtin |
The set of names that are required (no default) in at least one of the given functions. |
|
|
Get a function with some parameters removed. |
|
|
|
|
|
Wrap a function, optionally transforming interface, input and output. |
|
Give |
Classes
|
Ingress that renames parameters: called with outer names, returns inner (args, kwargs). |
|
Ingress with |
|
The Ingress class offers a template for creating ingress classes. |
|
Build an ingress from the wrapped function's signature and a spec of changes to it. |
|
Used to indicate that an object should be made as a function of an input func |
Mixin whose |
|
|
Placeholder default for a parameter whose value |
|
A function wrapper with interface modifiers. |
|
An extended wrapping object that allows more complex wrapping mechanisms. |
Exceptions
Raised when a caller is not valid |
|
Raised when a egress is not valid |
|
Raised when a ingress is not valid |
|
Raised when wrapper some construction params are not valid |
- i2.wrapper.AUTO_PRESERVE_SIGNATURE = 'auto'¶
preserve_signaturevalue 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:
objectIngress 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_mapperis the function form.
- class i2.wrapper.ArgValConverterIngress(func, _ArgValConverterIngress__strict=True, **conversion_for_arg)[source]¶
Bases:
objectIngress with
func’s signature that appliesname=converterfunctions to arguments.Names that are not parameters of
funcare rejected with anAssertionErrorat construction (the__strictparameter cannot be passed by keyword from outside the class, since the name is mangled; usearg_val_converter_ingressto switch it off).
- exception i2.wrapper.CallerValidationError[source]¶
Bases:
WrapperValidationErrorRaised when a caller is not valid
- exception i2.wrapper.EgressValidationError[source]¶
Bases:
WrapperValidationErrorRaised 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:
objectThe 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
Ingressclass 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 outerwis used to compute the innerwandx)many-to-1(e.g. the outerxandyare used to compute innery)
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
Ingressclass.>>> 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'
- exception i2.wrapper.IngressValidationError[source]¶
Bases:
WrapperValidationErrorRaised when a ingress is not valid
- class i2.wrapper.InnerMapIngress(inner_sig, kwargs_trans=None, *, _allow_reordering=False, **changes_for_name)[source]¶
Bases:
objectBuild 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:
InnerMapIngressprovides 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 iskwargs_transisouter_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_namepairs, thedict_of_changes_for_that_nameis adictwith keys being validinspect.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 ** zSay we wanted a version of this function
that didn’t have the argument kind restrinctions (all POSITION_OR_KEYWORD),
where the annotation of
xwas changedintand the default removedwhere
ywas namedyouinstead, and has an annotation (int).where the default of
zwas10instead of3, 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,
InnerMapIngresscomes in handy.With
InnerMapIngresswe’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
Parameterobject 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
NonesinceNoneis both a valid default and a valid annotation; So instead you need to useParameter.empty(conveniently assigned to a constant namedemptyin thewrappingmodule.
Now see that all arguments are
POSITIONAL_OR_KEYWORD,xandyareint, and default ofzis 10:>>> assert ( ... str(signature(ingress)) ... == str(signature(directly_defined_ingress)) ... == '(w, x: int, you: int = 2, z=10)' ... )Additionally,
ingressfunction 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_sigto reorder arguments.
- Returns:
An ingress that will allow one to use a function having the
inner_sigsignature 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 ** zThat 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
htakes on the signature (kind, defaults, and annotations) ofg. Let’s see now thathactually computes, uses the defaults ofgand can doesn’t have the position only restriction onw.>>> 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:
objectUsed to indicate that an object should be made as a function of an input func
- class i2.wrapper.PickleHelperMixin[source]¶
Bases:
objectMixin whose
__reduce__pickles an instance by its__name__(a global reference).
- class i2.wrapper.SmartDefault(func_computing_default, original_default)[source]¶
Bases:
objectPlaceholder default for a parameter whose value
add_smart_defaultscomputes 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:
_WrapA 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
Wrapinstancename – 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)signatureTrue: 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
ingressfunction 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 onfuncto 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 10Pattern 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) 6Pattern 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) -> intPitfall 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) TruePitfall 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 # CORRECTPitfall 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
wrapfunction.
- exception i2.wrapper.WrapperValidationError[source]¶
Bases:
ValueErrorRaised when wrapper some construction params are not valid
- class i2.wrapper.Wrapx(func, ingress=None, egress=None, *, caller=None, name=None)[source]¶
Bases:
_WrapAn 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
Wrapinstancecaller – A caller defines what it means to call the
funcon the arguments it is given. It should be of the formcaller(func, args, kwargs, *, ...extra_keyword_only_params). By default, the caller will simply returnfunc(*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
funcand thefunc_argsandfunc_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)ifcond(k, v)is true, elsevunchanged.
- i2.wrapper.arg_val_converter(func, **conversion_for_arg)[source]¶
Wrap
funcso 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.complete_dict_applying_functions(d, /, _only_if_name_missing=True, _allow_overwrites=False, **func_for_name)[source]¶
Complete dict
dby applying function to variables ind, sequentially.That is, doing
d[name] = func(**d)for allname, func in d.items().Set
_allow_overwrites=Trueto allow overwrites.Set
_only_if_name_missing=Falseto apply all functions offunc_for_nameregardless if thenamealready exists indor 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
bis present in inputdict, it’s value is conserved. That is, theboffunc_for_nameisn’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_functionswill try to compute everythingfunc_for_nametells 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 bIf 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 ofto_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, candd.- Return type:
>>> 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
aand it’scfrom 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”), thataandcare not there, but that the two remaining parameters,banddare present, in the same order, and with the same annotations and parameter kind (thedis still keyword-only).Now let’s make a dummy object that has attributes
aand ac, and use it to callmethod_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_namesto 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.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
ValueErrorif 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 dictkey_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_funcshould be applied “recursively”. When set toFalse(default), each transformation function applies to the originalkwargs, not the one that was transformed, so far._inplace – If set to
Falsewill make a shallow copy of thekwargsbefore transforming it (only relevant if_recursive=Truekey_and_val_func – The
key=val_funcpairs that indicate that aval_funcshould be applied to thekwargs, maching the argument names of theval_functo the keys ofkwargsand extracting the values found therein to use for the corresponding inputs of thatval_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
dis unchanged here (transformation is not in place).>>> d {'a': 1, 'b': 2, 'c': 3}Typically you’ll use
kwargs_transas 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
_recursiveand_inplacearguments 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,ditself 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_paramobject out ofouter_kwargsand sourceattr_namesfrom 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=Truekeeps them.
- i2.wrapper.modify_dict_on_cond(d, cond, func)[source]¶
Copy
d, applyingfuncto the values whose(key, value)satisfycond.
- i2.wrapper.move_names_to_the_end(names, names_to_move_to_the_end)[source]¶
Remove the items of
names_to_move_to_the_endfromnamesand 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)tupledataclasses.make_dataclassexpects 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.partialwith the ability toset
__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 * cNote that
agets a default, butbdoes not, yet is aftera. 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'