dol.sig_compare¶
Compare signatures, and check whether calls to one function are valid calls to another.
compare_signatures,param_comparator,keyed_comparator: comparison toolsparam_differences_dict,SigPair: describe how two signatures differis_call_compatible_with: is every valid call to one signature a valid call to the other? (Sig.is_call_compatible_withcalls it)
It used to be part of dol.signatures, which still re-exports all of it
(i2mint/dol#70).
>>> from dol.sig_compare import is_call_compatible_with
>>> from dol.signatures import Sig
>>> is_call_compatible_with(Sig('(a, b)'), Sig('(a, b, c=3)'))
True
>>> is_call_compatible_with(Sig('(a, b, c=3)'), Sig('(a, b)'))
False
Functions
|
|
|
Check if two defaults are the same when they are not empty. |
|
Why such a strange default comparison function? |
|
|
|
Return True if |
|
Create a key-function enabled binary operator. |
|
|
|
|
|
Compare two parameters. |
|
Compare two parameters. |
|
Makes a dictionary exibiting the differences between two parameters. |
|
A decorator that will process the output of the wrapped function with egress |
|
Classes
|
Class that operates on a pair of signatures. |
- class dol.sig_compare.SigPair(sig1, sig2)[source]¶
Bases:
objectClass that operates on a pair of signatures.
For example, offers methods to compare two signatures in various ways.
- Parameters:
>>> from pprint import pprint >>> def three(a, b: int, c=3): ... >>> def little(a, *, b=2, d=4) -> int: ... >>> def pigs(a, b) -> int: ... >>> sig_pair = SigPair(three, little) >>> >>> sig_pair.shared_names ['a', 'b'] >>> sig_pair.names_missing_in_sig1 ['d'] >>> sig_pair.names_missing_in_sig2 ['c'] >>> sig_pair.param_comparison() False >>> pprint(sig_pair.diff()) {'names_missing_in_sig1': ['d'], 'names_missing_in_sig2': ['c'], 'param_differences': {'b': {'annotation': (<class 'int'>, <class 'inspect._empty'>), 'default': (<class 'inspect._empty'>, 2), 'kind': (<_ParameterKind.POSITIONAL_OR_KEYWORD: 1>, <_ParameterKind.KEYWORD_ONLY: 3>)}}, 'return_annotation': (<class 'inspect._empty'>, <class 'int'>)}
Call compatibility says that any arguments leading to a valid call to a function having the first signature, will also lead to a valid call to a function having the second signature. This is not the case for the signatures of
threeandlittle:>>> sig_pair.are_call_compatible() False
But we don’t need to have equal signatures to have call compatibility. For example,
>>> SigPair(three, lambda a, b=2, c=30: None).are_call_compatible() True
Note that call-compatibility is not symmetric. For example,
pigsis call compatible withthree, since any arguments that are valid forpigsare valid forthree:>>> SigPair(pigs, three).are_call_compatible() True
But
threeis not call-compatible withpigssincethreerequires could include acargument, whichpigswould choke on.>>> SigPair(three, pigs).are_call_compatible() False
- are_call_compatible(param_comparator=None)[source]¶
Check if the signatures are call-compatible.
Returns True if sig1 can be used to call sig2 or vice versa.
>>> sig1 = Sig(lambda a, b, c=3: None) >>> sig2 = Sig(lambda a, b: None) >>> comp = SigPair(sig1, sig2) >>> comp.are_call_compatible() False
>>> comp = SigPair(sig2, sig1) >>> comp.are_call_compatible() True
- Return type:
- diff()[source]¶
Get a dictionary of differences between the two signatures.
>>> from pprint import pprint >>> def three(a, b: int, c=3): ... >>> def little(a, *, b=2, d=4) -> int: ... >>> def pigs(a, b: int = 2) -> int: ... >>> pprint(SigPair(three, little).diff()) {'names_missing_in_sig1': ['d'], 'names_missing_in_sig2': ['c'], 'param_differences': {'b': {'annotation': (<class 'int'>, <class 'inspect._empty'>), 'default': (<class 'inspect._empty'>, 2), 'kind': (<_ParameterKind.POSITIONAL_OR_KEYWORD: 1>, <_ParameterKind.KEYWORD_ONLY: 3>)}}, 'return_annotation': (<class 'inspect._empty'>, <class 'int'>)} >>> pprint(SigPair(three, pigs).diff()) {'names_missing_in_sig2': ['c'], 'param_differences': {'b': {'default': (<class 'inspect._empty'>, 2)}}, 'return_annotation': (<class 'inspect._empty'>, <class 'int'>)} >>> pprint(SigPair(three, three).diff()) {}
- Return type:
- diff_str()[source]¶
Get a string representation of the differences between the two signatures.
- Return type:
- property names_missing_in_sig1¶
List of names that are in the sig2 signature but not in sig1.
>>> sig1 = Sig(lambda a, b, c: None) >>> sig2 = Sig(lambda b, c, d: None) >>> comp = SigPair(sig1, sig2) >>> comp.names_missing_in_sig1 ['d']
- property names_missing_in_sig2¶
List of names that are in the sig1 signature but not in sig2.
>>> sig1 = Sig(lambda a, b, c: None) >>> sig2 = Sig(lambda b, c, d: None) >>> comp = SigPair(sig1, sig2) >>> comp.names_missing_in_sig2 ['a']
- param_comparison(comparator=<function param_comparator>, aggregation=<built-in function all>)[source]¶
Compare parameters between the two signatures using the provided comparator function.
- Parameters:
comparator – A function to compare two parameters.
aggregation – A function to aggregate the results of the comparisons.
- Return type:
- Returns:
Boolean result of the aggregated comparisons.
>>> sig1 = Sig('(a, b: int, c=3)') >>> sig2 = Sig('(a, *, b=2, d=4)') >>> comp = SigPair(sig1, sig2) >>> comp.param_comparison() False
- param_differences()[source]¶
Get a dictionary of parameter differences between the two signatures.
- Return type:
- Returns:
A dict containing differences for each shared param that has any.
>>> sig1 = Sig('(a, b: int, c=3)') >>> sig2 = Sig('(a, *, b=2, d=4)') >>> comp = SigPair(sig1, sig2) >>> result = comp.param_differences() >>> expected = { ... 'b': { ... 'kind': (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY), ... 'default': (Parameter.empty, 2), ... 'annotation': (int, Parameter.empty), ... } ... } >>> result == expected True
List of names that are common to both signatures, in the order of sig1.
>>> sig1 = Sig(lambda a, b, c: None) >>> sig2 = Sig(lambda b, c, d: None) >>> comp = SigPair(sig1, sig2) >>> comp.shared_names ['b', 'c']
- dol.sig_compare.defaults_are_the_same_when_not_empty(dflt1, dflt2)[source]¶
Check if two defaults are the same when they are not empty.
>>> defaults_are_the_same_when_not_empty(1, 1) True >>> defaults_are_the_same_when_not_empty(1, 2) False >>> defaults_are_the_same_when_not_empty(1, None) False >>> defaults_are_the_same_when_not_empty(1, Parameter.empty) True
- dol.sig_compare.dflt1_is_empty_or_dflt2_is_not(dflt1, dflt2)[source]¶
Why such a strange default comparison function?
This is to be used as a default in is_call_compatible_with.
Consider two functions func1 and func2 with a parameter p with default values dflt1 and dflt2 respectively. If dflt1 was not empty and dflt2 was, this would mean that func1 could be called without specifying p, but func2 couldn’t.
So to avoid this situation, we use dflt1_is_empty_or_dflt2_is_not as the default
- dol.sig_compare.dflt1_is_empty_or_dflt2_is_not_param_comparator(param1, param2, *, name=<function ignore_any_differences>, kind=<function ignore_any_differences>, default=<function dflt1_is_empty_or_dflt2_is_not>, annotation=<function ignore_any_differences>, aggreg=<built-in function all>)¶
Permissive version of param_comparator that ignores any differences of parameter attributes.
It is meant to be used with partial, but with a permissive base, contrary to the base param_comparator which requires strict equality (
eq) for all attributes.- Return type:
Comparison
- dol.sig_compare.is_call_compatible_with(sig1, sig2, *, param_comparator=None)[source]¶
Return True if
sig1is compatible withsig2. Meaning that all valid ways to callsig1are valid forsig2.- Parameters:
- Return type:
>>> is_call_compatible_with( ... Sig('(a, /, b, *, c)'), ... Sig('(a, b, c)') ... ) True >>> is_call_compatible_with( ... Sig('()'), ... Sig('(a)') ... ) False >>> is_call_compatible_with( ... Sig('()'), ... Sig('(a=0)') ... ) True >>> is_call_compatible_with( ... Sig('(a, /, *, c)'), ... Sig('(a, /, b, *, c)') ... ) False >>> is_call_compatible_with( ... Sig('(a, /, *, c)'), ... Sig('(a, /, b=0, *, c)') ... ) True >>> is_call_compatible_with( ... Sig('(a, /, b)'), ... Sig('(a, /, b, *, c)') ... ) False >>> is_call_compatible_with( ... Sig('(a, /, b)'), ... Sig('(a, /, b, *, c=0)') ... ) True >>> is_call_compatible_with( ... Sig('(a, /, b, *, c)'), ... Sig('(*args, **kwargs)') ... ) True
- dol.sig_compare.keyed_comparator(comparator, key)[source]¶
Create a key-function enabled binary operator.
In various places in python functionality is extended by allowing a key function. For example, the
sortedfunction allows a key function to be passed, which is applied to each element before sorting. The keyed_comparator function allows a comparator to be extended in the same way. The returned comparator will apply the key function toeach input before applying the original comparator.>>> from operator import eq >>> parity = lambda x: x % 2 >>> comparator = keyed_comparator(eq, parity) >>> list(map(comparator, [1, 1, 2, 2], [3, 4, 5, 6])) [True, False, False, True]
- Return type:
Callable[[Compared,Compared],Comparison]
- dol.sig_compare.param_binary_func(param1, param2, *, name=<built-in function eq>, kind=<built-in function eq>, default=<built-in function eq>, annotation=<built-in function eq>, aggreg=<built-in function all>)¶
Compare two parameters.
Note that by default, this function is strict, and will return False if any of the parameters are not equal. This is because the default aggregation function is
alland the default comparison functions of the parameter’s attributes areeq(meaning equality, not identity).But you can change that by passing different comparison functions and/or aggregation functions.
In fact, the real purpose of this function is to be used as a factory of parameter binary functions, through parametrizing it with
functools.partial.The parameter binary functions themselves are meant to be used to make signature binary functions.
- Parameters:
param1 (
Parameter) – first parameterparam2 (
Parameter) – second parametername (
Callable[[Compared,Compared],Comparison]) – function to compare nameskind (
Callable[[Compared,Compared],Comparison]) – function to compare kindsdefault (
Callable[[Compared,Compared],Comparison]) – function to compare defaultsannotation (
Callable[[Compared,Compared],Comparison]) – function to compare annotationsaggreg (
Callable[[Iterable[Comparison]],Any]) – function to aggregate results
- Return type:
Comparison
>>> from inspect import Parameter >>> param1 = Parameter('x', Parameter.POSITIONAL_OR_KEYWORD) >>> param2 = Parameter('x', Parameter.POSITIONAL_OR_KEYWORD) >>> param_binary_func(param1, param2) True
See https://github.com/i2mint/i2/issues/50#issuecomment-1381686812 for discussion.
- dol.sig_compare.param_comparator(param1, param2, *, name=<built-in function eq>, kind=<built-in function eq>, default=<built-in function eq>, annotation=<built-in function eq>, aggreg=<built-in function all>)[source]¶
Compare two parameters.
Note that by default, this function is strict, and will return False if any of the parameters are not equal. This is because the default aggregation function is
alland the default comparison functions of the parameter’s attributes areeq(meaning equality, not identity).But you can change that by passing different comparison functions and/or aggregation functions.
In fact, the real purpose of this function is to be used as a factory of parameter binary functions, through parametrizing it with
functools.partial.The parameter binary functions themselves are meant to be used to make signature binary functions.
- Parameters:
param1 (
Parameter) – first parameterparam2 (
Parameter) – second parametername (
Callable[[Compared,Compared],Comparison]) – function to compare nameskind (
Callable[[Compared,Compared],Comparison]) – function to compare kindsdefault (
Callable[[Compared,Compared],Comparison]) – function to compare defaultsannotation (
Callable[[Compared,Compared],Comparison]) – function to compare annotationsaggreg (
Callable[[Iterable[Comparison]],Any]) – function to aggregate results
- Return type:
Comparison
>>> from inspect import Parameter >>> param1 = Parameter('x', Parameter.POSITIONAL_OR_KEYWORD) >>> param2 = Parameter('x', Parameter.POSITIONAL_OR_KEYWORD) >>> param_binary_func(param1, param2) True
See https://github.com/i2mint/i2/issues/50#issuecomment-1381686812 for discussion.
- dol.sig_compare.param_comparison_dict(param1, param2, *, name=<function return_tuple>, kind=<function return_tuple>, default=<function return_tuple>, annotation=<function return_tuple>, aggreg=<function param_attribute_dict>)¶
A ParamComparator that returns a dictionary with pairs parameter attributes.
>>> param1 = Sig('(a: int = 1)')['a'] >>> param2 = Sig('(a: str = 2)')['a'] >>> param_comparison_dict(param1, param2) {'name': ('a', 'a'), 'kind': ..., 'default': (1, 2), 'annotation': (<class 'int'>, <class 'str'>)}
- Return type:
Comparison
- dol.sig_compare.param_differences_dict(param1, param2, *, name=<built-in function eq>, kind=<built-in function eq>, default=<built-in function eq>, annotation=<built-in function eq>)[source]¶
Makes a dictionary exibiting the differences between two parameters.
>>> param1 = Sig('(a: int = 1)')['a'] >>> param2 = Sig('(a: str = 2)')['a'] >>> param_differences_dict(param1, param2) {'default': (1, 2), 'annotation': (<class 'int'>, <class 'str'>)} >>> param_differences_dict(param1, param2, default=lambda x, y: isinstance(x, type(y))) {'annotation': (<class 'int'>, <class 'str'>)}
- dol.sig_compare.permissive_param_comparator(param1, param2, *, name=<function ignore_any_differences>, kind=<function ignore_any_differences>, default=<function ignore_any_differences>, annotation=<function ignore_any_differences>, aggreg=<built-in function all>)¶
Permissive version of param_comparator that ignores any differences of parameter attributes.
It is meant to be used with partial, but with a permissive base, contrary to the base param_comparator which requires strict equality (
eq) for all attributes.- Return type:
Comparison