dol.sig_recipes¶
Recipes built on Sig: make signatures, and change the signature of functions.
mk_sig_from_args,params_of,insert_annotations: make or edit signaturescall_forgivingly,call_somewhat_forgivingly: call a function with the arguments it accepts, ignoring the othersall_pk_signature,ch_func_to_all_pk,ch_variadics_to_non_variadic_kind,kind_forgiving_func,normalized_func: change the kinds of a function’s parametersset_signature_of_func,replace_kwargs_using,use_interface: give a function another signaturesig_to_dataclass,has_signature,copy_func,common_and_diff_argnames
It used to be part of dol.signatures, which still re-exports all of it
(i2mint/dol#70).
>>> from dol.sig_recipes import call_forgivingly
>>> def f(a, b=2):
... return a + b
>>> call_forgivingly(f, 1, b=3, c=4) # c is not a parameter of f: it is ignored
4
Functions
|
Changes all (non-variadic) arguments to be of the PK (POSITION_OR_KEYWORD) kind. |
|
Call function on given args and kwargs, but only taking what the function needs (not choking if they're extras variables) |
|
Call function on given args and kwargs, but with controllable argument leniency. |
|
Returns a decorated function where all arguments are of the PK kind. |
|
Changes all (non-variadic) arguments to be of the PK (POSITION_OR_KEYWORD) kind. |
|
A decorator that will change a VAR_POSITIONAL ( |
|
Get list of argument names that are common to two functions, as well as the two lists of names that are different |
|
|
|
Copy a function (not sure it works with all types of callables) |
|
Check if an object has a signature -- i.e. is callable and inspect.signature( obj) returns something. |
|
Insert annotations in a signature. |
|
Wraps the func, changing the argument kinds according to kinds_modifier. |
|
Make a Signature instance by specifying args_without_default and args_with_defaults. |
|
|
|
|
|
Decorator that replaces the variadic keyword argument of the target function using the |
|
Set the signature of a function, with sugar. |
|
Make a |
|
Use interface_sig as (enforced/validated) signature of the decorated function. |
- dol.sig_recipes.all_pk_signature(callable_or_signature)[source]¶
Changes all (non-variadic) arguments to be of the PK (POSITION_OR_KEYWORD) kind.
Wrapping a function with the resulting signature doesn’t make that function callable with PK kinds in itself. It just gives it a signature without position and keyword ONLY kinds. It should be used to wrap such a function that actually carries out the implementation though!
>>> def foo(w, /, x: float, y=1, *, z: int = 1, **kwargs): ... ... >>> def bar(*args, **kwargs): ... ... ... >>> from inspect import signature >>> new_foo = all_pk_signature(foo) >>> Sig(new_foo) <Sig (w, x: float, y=1, z: int = 1, **kwargs)> >>> all_pk_signature(signature(foo)) <Sig (w, x: float, y=1, z: int = 1, **kwargs)>
But note that the variadic arguments
*argsand**kwargsremain variadic:>>> all_pk_signature(signature(bar)) <Signature (*args, **kwargs)>
It works with
Sigtoo (since Sig is a Signature), and maintains it’s other attributes (like name).>>> sig = all_pk_signature(Sig(bar)) >>> sig <Sig (*args, **kwargs)> >>> sig.name 'bar'
See also
i2.signatures.kind_forgiving_func
- dol.sig_recipes.call_forgivingly(func, *args, **kwargs)[source]¶
Call function on given args and kwargs, but only taking what the function needs (not choking if they’re extras variables)
Tip
If you into trouble because your kwargs has a ‘func’ key, (which would then clash with the
funcparam of call_forgivingly), then use_call_forgivinglyinstead, specifying args and kwargs as tuple and dict.>>> def foo(a, b: int = 0, c=None) -> int: ... return "foo", (a, b, c) >>> call_forgivingly( ... foo, # the function you want to call ... "input for a", # meant for a -- the first (and only) argument foo requires ... c=42, # skiping b and giving c a non-default value ... intruder="argument", # but wait, this argument name doesn't exist! Oh no! ... ) # well, as it happens, nothing bad -- the intruder argument is just ignored ('foo', ('input for a', 0, 42))
An example of what happens when variadic kinds are involved:
>>> def bar(x, *args1, y=1, **kwargs1): ... return x, args1, y, kwargs1 >>> call_forgivingly(bar, 1, 2, 3, y=4, z=5) (1, (2, 3), 4, {'z': 5})
- dol.sig_recipes.call_somewhat_forgivingly(func, args, kwargs, enforce_sig=None)[source]¶
Call function on given args and kwargs, but with controllable argument leniency. By default, the function will only pick from args and kwargs what matches it’s signature, ignoring anything else in args and kwargs.
But the real use of
call_somewhat_forgivinglykicks in when you specify aenforce_sig: A signature (or any object that can be resolved into a signature throughSig(enforce_sig)) that will be used to bind the inputs, thus validating them against theenforce_sigsignature (including extra arguments, defaults, etc.).call_somewhat_forgivinglyhelps you do this kind of thing systematically.>>> f = lambda a: a * 11 >>> assert call_somewhat_forgivingly(f, (2,), {}) == f(2)
In the above, we have no
enforce_sig. The real use of call_somewhat_forgivingly is when we ask it to enforce a signature. Let’s do this by specifying a function (no need for it to do anything: Only the signature is used.>>> g = lambda a, b=None: ...
Calling
fon it’s normal set of inputs (one input in this case) gives you the same thing asf:>>> assert call_somewhat_forgivingly(f, (2,), {}, enforce_sig=g) == f(2) >>> assert call_somewhat_forgivingly(f, (), {'a': 2}, enforce_sig=g) == f(2)
If you call with an extra positional argument, it will just be ignored.
>>> assert call_somewhat_forgivingly(f, (2, 'ignored'), {}, enforce_sig=g) == f(2)
If you call with a
bkeyword-argument (which matchesg’s signature, it will also be ignored.>>> assert call_somewhat_forgivingly( ... f, (2,), {'b': 'ignored'}, enforce_sig=g ... ) == f(2) >>> assert call_somewhat_forgivingly( ... f, (), {'a': 2, 'b': 'ignored'}, enforce_sig=g ... ) == f(2)
But if you call with three positional arguments (one more than g allows), or call with a keyword argument that is not in
g’s signature, it will raise aTypeError:>>> call_somewhat_forgivingly(f, ... (2, 'ignored', 'does_not_fit_g_signature_anymore'), {}, enforce_sig=g ... ) Traceback (most recent call last): ... TypeError: too many positional arguments >>> call_somewhat_forgivingly(f, ... (2,), {'this_argname': 'is not in g'}, enforce_sig=g ... ) Traceback (most recent call last): ... TypeError: got an unexpected keyword argument 'this_argname'
- dol.sig_recipes.ch_func_to_all_pk(func)[source]¶
Returns a decorated function where all arguments are of the PK kind. (PK: Positional_or_keyword)
- Parameters:
func – A callable
- Returns:
>>> def f(a, /, b, *, c=None, **kwargs): ... return a + b * c ... >>> print(Sig(f)) (a, /, b, *, c=None, **kwargs) >>> ff = ch_func_to_all_pk(f) >>> print(Sig(ff)) (a, b, c=None, **kwargs) >>> ff(1, 2, 3) 7 >>> >>> def g(x, y=1, *args, **kwargs): ... ... ... >>> print(Sig(g)) (x, y=1, *args, **kwargs) >>> gg = ch_func_to_all_pk(g) >>> print(Sig(gg)) (x, y=1, args=(), **kwargs)
- dol.sig_recipes.ch_signature_to_all_pk(callable_or_signature)¶
Changes all (non-variadic) arguments to be of the PK (POSITION_OR_KEYWORD) kind.
Wrapping a function with the resulting signature doesn’t make that function callable with PK kinds in itself. It just gives it a signature without position and keyword ONLY kinds. It should be used to wrap such a function that actually carries out the implementation though!
>>> def foo(w, /, x: float, y=1, *, z: int = 1, **kwargs): ... ... >>> def bar(*args, **kwargs): ... ... ... >>> from inspect import signature >>> new_foo = all_pk_signature(foo) >>> Sig(new_foo) <Sig (w, x: float, y=1, z: int = 1, **kwargs)> >>> all_pk_signature(signature(foo)) <Sig (w, x: float, y=1, z: int = 1, **kwargs)>
But note that the variadic arguments
*argsand**kwargsremain variadic:>>> all_pk_signature(signature(bar)) <Signature (*args, **kwargs)>
It works with
Sigtoo (since Sig is a Signature), and maintains it’s other attributes (like name).>>> sig = all_pk_signature(Sig(bar)) >>> sig <Sig (*args, **kwargs)> >>> sig.name 'bar'
See also
i2.signatures.kind_forgiving_func
- dol.sig_recipes.ch_variadics_to_non_variadic_kind(func, *, ch_variadic_keyword_to_keyword=True)[source]¶
A decorator that will change a VAR_POSITIONAL (
*args) argument to a tuple (args) argument of the same name.Essentially, given a
func(a, *b, c, **d)function want to get anew_func(a, b=(), c=None, d={})that has the same functionality (in fact, calls the originalfuncfunction behind the scenes), but without where the variadic arguments*band**dare replaced with abexpecting an iterable (e.g. tuple/list) anddexpecting adictto contain the desired inputs.Besides this, the decorator tries to be as conservative as possible, making only the minimum changes needed to meet the goal of getting to a variadic-less interface. When it doubt, and error will be raised.
>>> def foo(a, *args, bar, **kwargs): ... return f"{a=}, {args=}, {bar=}, {kwargs=}" >>> assert str(Sig(foo)) == '(a, *args, bar, **kwargs)' >>> wfoo = ch_variadics_to_non_variadic_kind(foo) >>> str(Sig(wfoo)) '(a, args=(), *, bar, kwargs={})'
And now to do this:
>>> foo(1, 2, 3, bar=4, hello="world") "a=1, args=(2, 3), bar=4, kwargs={'hello': 'world'}"
We can do it like this instead:
>>> wfoo(1, (2, 3), bar=4, kwargs=dict(hello="world")) "a=1, args=(2, 3), bar=4, kwargs={'hello': 'world'}"
Note, the outputs are the same. It’s just the way we call our function that has changed.
>>> assert wfoo(1, (2, 3), bar=4, kwargs=dict(hello="world") ... ) == foo(1, 2, 3, bar=4, hello="world") >>> assert wfoo(1, (2, 3), bar=4) == foo(1, 2, 3, bar=4) >>> assert wfoo(1, (), bar=4) == foo(1, bar=4)
Note that if there is not variadic positional arguments, the variadic keyword will still be a keyword-only kind.
>>> @ch_variadics_to_non_variadic_kind ... def func(a, bar=None, **kwargs): ... return f"{a=}, {bar=}, {kwargs=}" >>> str(Sig(func)) '(a, bar=None, *, kwargs={})' >>> assert func(1, bar=4, kwargs=dict(hello="world") ... ) == "a=1, bar=4, kwargs={'hello': 'world'}"
If the function has neither variadic kinds, it will remain untouched.
>>> def func(a, /, b, *, c=3): ... return a + b + c >>> ch_variadics_to_non_variadic_kind(func) == func True
If you only want the variadic positional to be handled, but leave leave any VARIADIC_KEYWORD kinds (
**kwargs) alone, you can do so by settingch_variadic_keyword_to_keyword=False. If you’ll need to usech_variadics_to_non_variadic_kindin such a way repeatedly, we suggest you usefunctools.partialto not have to specify this configuration repeatedly.>>> from functools import partial >>> tuple_the_args = partial(ch_variadics_to_non_variadic_kind, ... ch_variadic_keyword_to_keyword=False ... ) >>> @tuple_the_args ... def foo(a, *args, bar=None, **kwargs): ... return f"{a=}, {args=}, {bar=}, {kwargs=}" >>> Sig(foo) <Sig (a, args=(), *, bar=None, **kwargs)> >>> foo(1, (2, 3), bar=4, hello="world") "a=1, args=(2, 3), bar=4, kwargs={'hello': 'world'}"
- dol.sig_recipes.common_and_diff_argnames(func1, func2)[source]¶
Get list of argument names that are common to two functions, as well as the two lists of names that are different
- Parameters:
func1 (
callable) – First functionfunc2 (
callable) – Second function
- Return type:
- Returns:
A dict with fields ‘common’, ‘func1_not_func2’, and ‘func2_not_func1’
>>> def f(t, h, i, n, k): ... ... ... >>> def g(t, w, i, c, e): ... ... ... >>> common_and_diff_argnames(f, g) {'common': ['t', 'i'], 'func1_not_func2': ['h', 'n', 'k'], 'func2_not_func1': ['w', 'c', 'e']} >>> common_and_diff_argnames(g, f) {'common': ['t', 'i'], 'func1_not_func2': ['w', 'c', 'e'], 'func2_not_func1': ['h', 'n', 'k']}
- dol.sig_recipes.copy_func(f)[source]¶
Copy a function (not sure it works with all types of callables)
- dol.sig_recipes.has_signature(obj, robust=False)[source]¶
Check if an object has a signature – i.e. is callable and inspect.signature( obj) returns something.
This can be used to more easily get signatures in bulk without having to write try/catches:
>>> from functools import partial >>> len( ... list( ... filter( ... None, ... map( ... partial(has_signature, robust=False), ... (Sig, print, map, filter, Sig.wrap), ... ), ... ) ... ) ... ) 2
If robust is set to True,
has_signaturewill useSigto get the signature, so will return True in most cases.
- dol.sig_recipes.insert_annotations(s, /, *, return_annotation, **annotations)[source]¶
Insert annotations in a signature. (Note: not really insert but returns a copy of input signature)
>>> from inspect import signature >>> s = signature(lambda a, b, c=1, d="bar": 0) >>> s <Signature (a, b, c=1, d='bar')> >>> ss = insert_annotations(s, b=int, d=str) >>> ss <Signature (a, b: int, c=1, d: str = 'bar')> >>> insert_annotations(s, b=int, d=str, e=list) Traceback (most recent call last): ... AssertionError: These argument names weren't found in the signature: {'e'}
- dol.sig_recipes.kind_forgiving_func(func, kinds_modifier=<function convert_to_PK>)[source]¶
Wraps the func, changing the argument kinds according to kinds_modifier. The default behaviour is to change all kinds to POSITIONAL_OR_KEYWORD kinds. The original purpose of this function is to remove argument-kind restriction annoyances when doing functional manipulations such as:
>>> from functools import partial >>> isinstance_of_str = partial(isinstance, class_or_tuple=str) >>> isinstance_of_str('I am a string') Traceback (most recent call last): ... TypeError: isinstance() takes no keyword arguments
Here, instead, we can just get a kinder version of the function and do what we want to do:
>>> _isinstance = kind_forgiving_func(isinstance) >>> isinstance_of_str = partial(_isinstance, class_or_tuple=str) >>> isinstance_of_str('I am a string') True >>> isinstance_of_str(42) False
See also
i2.signatures.all_pk_signature
- dol.sig_recipes.mk_sig_from_args(*args_without_default, **args_with_defaults)[source]¶
Make a Signature instance by specifying args_without_default and args_with_defaults.
>>> mk_sig_from_args("a", "b", c=1, d="bar") <Signature (a, b, c=1, d='bar')>
- dol.sig_recipes.replace_kwargs_using(sig)[source]¶
Decorator that replaces the variadic keyword argument of the target function using the
sig, the signature of a source function. It essentially injects the difference betweensigand the target function’s signature into the target function’s signature. That is, it replaces the variadic keyword argument (a.k.a. “kwargs”) with those parameters that are insigbut not in the target function’s signature.This is meant to be used when a
targ_func(the function you’ll apply the decorator to) has a variadict keyword argument that is just used to forward “extra” arguments to another function, and you want to make sure that the signature of thetarg_funcis consistent with thesigsignature. (Also, you don’t want to copy the signatures around manually.)In the following,
sauce(the target function) has a variadic keyword argument,sauce_kwargs, that is used to forward extra arguments toapple(the source function).>>> def apple(a, x: int, y=2, *, z=3, **extra_apple_options): ... return a + x + y + z >>> @replace_kwargs_using(apple) ... def sauce(a, b, c, **sauce_kwargs): ... return b * c + apple(a, **sauce_kwargs)
The function will works:
>>> sauce(1, 2, 3, x=4, z=5) # func still works? Should be: 1 + 4 + 2 + 5 + 2 * 3 18
But the signature now doesn’t have the
**sauce_kwargs, but more informative signature elements sourced fromapple:>>> Sig(sauce) <Sig (a, b, c, *, x: int, y=2, z=3, **extra_apple_options)>
One thing to note is that the order of the arguments in the signature of
applemay change to accomodate for the python parameter order rules (see https://docs.python.org/3/reference/compound_stmts.html#function-definitions). The new order will try to conserve the order of the original arguments ofsaucein-so-far as it doesn’t violate the python parameter order rules, though. See examples below:>>> @Sig.replace_kwargs_using(apple) ... def sauce(a, b=2, c=3, **sauce_kwargs): ... return b * c + apple(a, **sauce_kwargs) >>> Sig(sauce) <Sig (a, b=2, c=3, *, x: int, y=2, z=3, **extra_apple_options)>
>>> @Sig.replace_kwargs_using(apple) ... def sauce(a=1, b=2, c=3, **sauce_kwargs): ... return b * c + apple(a, **sauce_kwargs) >>> Sig(sauce) <Sig (a=1, b=2, c=3, *, x: int, y=2, z=3, **extra_apple_options)>
- dol.sig_recipes.set_signature_of_func(func, parameters, *, return_annotation, __validate_parameters__=True)[source]¶
Set the signature of a function, with sugar.
- Parameters:
func – Function whose signature you want to set
signature – A list of parameter specifications. This could be an
that (inspect.Parameter object or anything) – the mk_param function can resolve into an inspect.Parameter object.
return_annotation – Passed on to inspect.Signature.
__validate_parameters__ – Passed on to inspect.Signature.
- Returns:
None (but sets the signature of the input function)
>>> import inspect >>> def foo(*args, **kwargs): ... pass ... >>> inspect.signature(foo) <Signature (*args, **kwargs)> >>> set_signature_of_func(foo, ["a", "b", "c"]) >>> inspect.signature(foo) <Signature (a, b, c)> >>> set_signature_of_func( ... foo, ["a", ("b", None), ("c", 42, int)] ... ) # specifying defaults and annotations >>> inspect.signature(foo) <Signature (a, b=None, c: int = 42)> >>> set_signature_of_func( ... foo, ["a", "b", "c"], return_annotation=str ... ) # specifying return annotation >>> inspect.signature(foo) <Signature (a, b, c) -> str> >>> # But you can always specify parameters the "long" way >>> set_signature_of_func( ... foo, ... [inspect.Parameter(name="kws", kind=inspect.Parameter.VAR_KEYWORD)], ... return_annotation=str, ... ) >>> inspect.signature(foo) <Signature (**kws) -> str>
- dol.sig_recipes.sig_to_dataclass(sig, *, cls_name=None, bases=(), module=None, **kwargs)[source]¶
Make a
class(throughmake_dataclass) from the given signature.- Parameters:
sig (
Union[Signature,Iterable[Parameter],Mapping[str,Parameter],Callable,str]) – ASignatureAble, that is, anything that ensure_signature can resolve into aninspect.Signatureobject, including a signature object itself, but also most callables, a list or params, etc.cls_name – The same as
cls_nameofdataclasses.make_dataclassbases – The same as
basesofdataclasses.make_dataclassmodule – Set to module (usually
__name__to specify ther module of caller) so that the class and instances can be pickle-able.kwargs – Passed on to
dataclasses.make_dataclass
- Returns:
A dataclass
>>> def foo(a, /, b : int=2, *, c=3): ... pass ... >>> K = sig_to_dataclass(foo, cls_name='K') >>> str(Sig(K)) '(a, b: int = 2, c=3) -> None' >>> k = K(1,2,3) >>> (k.a, k.b, k.c) (1, 2, 3)
Would also work with any of these (and more):
>>> K = sig_to_dataclass(Sig(foo), cls_name='K') >>> K = sig_to_dataclass(Sig(foo).params, cls_name='K')
Note
cls_nameis not required (we’ll try to figure out a good default for you), but it’s advised to only use this convenience in extreme mode. Choosing your own name might make for a safer future if you’re reusing your class.
- dol.sig_recipes.tuple_the_args(func, *, ch_variadic_keyword_to_keyword=False)¶
A decorator that will change a VAR_POSITIONAL (*args) argument to a tuple (args) argument of the same name.
- dol.sig_recipes.use_interface(interface_sig)[source]¶
Use interface_sig as (enforced/validated) signature of the decorated function. That is, the decorated function will use the original function has the backend, the function actually doing the work, but with a frontend specified (in looks and in argument validation)
interface_sigconsider the situation where are functionality is parametrized by a function
gtaking two inputs,a, andb. Now you want to carry out this functionality using a functionfthat does whatgshould do, but doesn’t usea, and doesn’t even have it in it’s arguments.The solution to this is to _adapt_
fto theginterface:def my_g(a, b): return f(a)
and use
my_g.>>> f = lambda a: a * 11 >>> interface = lambda a, b=None: ... >>> >>> new_f = use_interface(interface)(f)
See how only the first argument, or
akeyword argument, is taken into account innew_f:>>> assert new_f(2) == f(2) >>> assert new_f(2, 3) == f(2) >>> assert new_f(2, b=3) == f(2) >>> assert new_f(b=3, a=2) == f(2)
But if we add more positional arguments than
interfaceallows, or any keyword arguments thatinterfacedoesn’t recognize…>>> new_f(1,2,3) Traceback (most recent call last): ... TypeError: too many positional arguments >>> new_f(1, c=2) Traceback (most recent call last): ... TypeError: got an unexpected keyword argument 'c'