How can I convert a typing.Callable
type hint into a inspect.Signature
function signature object?
Use case
Let's say I have a custom type hint for a function signature hint
:
hint = typing.Callable[[int], float]
I want to use it both for type hinting as well as finding functions which conform to the signature of hint
.
To achieve the latter, I could compare inspect.Signature
objects from the functions in question to hint
if I had a way of converting the type hint into a signature.
CodePudding user response:
This is an interesting question. I think there are two possible approaches, one of which I would recommend.
Construct Signature
from Callable
This is possible, but I would not recommend this.
The problem is that the Callable
annotation contains only a subset of the information that a Signature
object can hold. Information not available on a specified Callable
type includes:
- Parameter names
- Parameter kinds (e.g. positional only or keyword-only etc.)
- Default values
This means you have to make a lot of arbitrary choices, when you construct a Parameter
from a type.
Here is an example implementation:
from collections.abc import Callable
from inspect import Parameter, Signature, signature
from typing import get_args
def callable_type_to_signature(callable_type: type) -> Signature:
params, ret = get_args(callable_type)
params = [
Parameter(f"arg{i}", Parameter.POSITIONAL_ONLY, annotation=param)
for i, param in enumerate(params)
]
return Signature(params, return_annotation=ret)
def foo(x: int, y: str, z: dict[str, bool]) -> float:
return NotImplemented
if __name__ == '__main__':
hint_foo = Callable[[int, str, dict[str, bool]], float]
sig = callable_type_to_signature(hint_foo)
print(sig)
print(signature(foo))
Output:
(arg0: int, arg1: str, arg2: dict[str, bool], /) -> float
(x: int, y: str, z: dict[str, bool]) -> float
Notice that I chose to define all parameters as positional only and give them all names like argX
.
You could still use this to compare some function signatures with the output of this callable_type_to_signature
, but you would have to take care to not compare apples to oranges.
I think there is a better way.
Compare Signature
to Callable
Since you wanted to compare signatures to type hints anyway, I think you don't need that extra step of creating another "fake" Signature
. We can try and compare the two objects directly. Here is a working example:
from collections.abc import Callable
from inspect import Parameter, Signature, signature
from typing import get_args, get_origin
def param_matches_type_hint(
param: Parameter,
type_hint: type,
strict: bool = False,
) -> bool:
"""
Returns `True` if the parameter annotation matches the type hint.
For this to be the case:
In `strict` mode, both must be exactly equal.
If both are specified generic types, they must be exactly equal.
If the parameter annotation is a specified generic type and
the type hint is an unspecified generic type,
the parameter type's origin must be that generic type.
"""
param_origin = get_origin(param.annotation)
type_hint_origin = get_origin(type_hint)
if (
strict or
(param_origin is None and type_hint_origin is None) or
(param_origin is not None and type_hint_origin is not None)
):
return param.annotation == type_hint
if param_origin is None and type_hint_origin is not None:
return False
return param_origin == type_hint
def signature_matches_type_hint(
sig: Signature,
type_hint: type,
strict: bool = False,
) -> bool:
"""
Returns `True` if the function signature and `Callable` type hint match.
For details about parameter comparison, see `param_matches_type_hint`.
"""
if get_origin(type_hint) != Callable:
raise TypeError("type_hint must be a `Callable` type")
type_params, return_type = get_args(type_hint)
if sig.return_annotation != return_type:
return False
if len(sig.parameters) != len(type_params):
return False
return all(
param_matches_type_hint(sig_param, type_param, strict=strict)
for sig_param, type_param
in zip(sig.parameters.values(), type_params)
)
def foo(x: int, y: str, z: dict[str, bool]) -> float:
return NotImplemented
def bar(x: dict[str, int]) -> bool:
return NotImplemented
def baz(x: list) -> bool:
return NotImplemented
if __name__ == '__main__':
hint_foo = Callable[[int, str, dict], float]
hint_bar = Callable[[dict], bool]
hint_baz = Callable[[list[str]], bool]
print(signature_matches_type_hint(signature(foo), hint_foo))
print(signature_matches_type_hint(signature(bar), hint_bar))
print(signature_matches_type_hint(signature(baz), hint_baz))
print(signature_matches_type_hint(signature(bar), hint_bar, strict=True))
Output:
True
True
False
False
Details and caveats:
This is a rather simplistic implementation. For one thing, it doesn't handle more "exotic" signatures, such as those that contain arbitrary keyword arguments (**kwargs
). It is not entirely clear how that should be annotated anyway.
This assumes that a more general function signature like that of baz
is not compatible with the more specific type hint hint_baz
. The other way around however, the more specific function signature like that of bar
is compatible with the more general type hint hint_bar
.
If you only want exact matches regarding types, you can use strict=True
.
Hope this helps a bit and puts you on the right track. Maybe if I find the time, I'll try to extend this and test it a bit.