Can you create a function with a specific signature without using eval?
Question:
I’ve written some code that inspects function signatures, and I would like to generate test cases for it. For this, I need to be able to construct objects that result in a given Signature
object when signature
is called on them. I want to avoid just eval
-ing spliced together strings for this. Is there some other method for generating functions or objects that behave like them with signature
?
Specifically, I have this code:
from inspect import signature, Parameter
from typing import Any, Callable, ValuesView
def passable(fun: Callable[..., Any], arg: str | int) -> bool:
if not callable(fun):
raise TypeError("Argument fun to passable() must be callable")
if isinstance(arg, str):
return kwarg_passable(fun, arg)
elif isinstance(arg, int):
return arg_passable(fun, arg)
else:
raise TypeError("Argument arg to passable() must be int or str")
def kwarg_passable(fun: Callable[..., Any], arg_key: str) -> bool:
assert callable(fun)
assert isinstance(arg_key, str)
params: ValuesView[Parameter] = signature(fun).parameters.values()
return any(
param.kind is Parameter.VAR_KEYWORD
or (
param.kind in [Parameter.KEYWORD_ONLY, Parameter.POSITIONAL_OR_KEYWORD]
and param.name == arg_key
)
for param in params
)
def arg_passable(fun: Callable[..., Any], arg_ix: int) -> bool:
assert callable(fun)
assert isinstance(arg_ix, int)
params: ValuesView[Parameter] = signature(fun).parameters.values()
return sum(
1
for param in params
if param.kind in [Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD]
) > arg_ix or any(param.kind is Parameter.VAR_POSITIONAL for param in params)
I want to test passable
on randomly generated dummy functions using Hypothesis.
Answers:
You can assign a Signature
object to the callable’s .__signature__
attribute:
import inspect
from inspect import Signature as S, Parameter as P
def f(x: int = 2): pass
print(inspect.signature(f))
new_param = P(name="y", kind=P.KEYWORD_ONLY, default="abc", annotation=str)
f.__signature__ = S(parameters=[new_param])
print(inspect.signature(f))
(x: int = 2)
(*, y: str = 'abc')
Though note that of course this won’t affect what happens if you try to call it; f(y="def")
will give you a TypeError: f() got an unexpected keyword argument 'y'
, which you can in turn work around with *args, **kwargs
.
I’ve written some code that inspects function signatures, and I would like to generate test cases for it. For this, I need to be able to construct objects that result in a given Signature
object when signature
is called on them. I want to avoid just eval
-ing spliced together strings for this. Is there some other method for generating functions or objects that behave like them with signature
?
Specifically, I have this code:
from inspect import signature, Parameter
from typing import Any, Callable, ValuesView
def passable(fun: Callable[..., Any], arg: str | int) -> bool:
if not callable(fun):
raise TypeError("Argument fun to passable() must be callable")
if isinstance(arg, str):
return kwarg_passable(fun, arg)
elif isinstance(arg, int):
return arg_passable(fun, arg)
else:
raise TypeError("Argument arg to passable() must be int or str")
def kwarg_passable(fun: Callable[..., Any], arg_key: str) -> bool:
assert callable(fun)
assert isinstance(arg_key, str)
params: ValuesView[Parameter] = signature(fun).parameters.values()
return any(
param.kind is Parameter.VAR_KEYWORD
or (
param.kind in [Parameter.KEYWORD_ONLY, Parameter.POSITIONAL_OR_KEYWORD]
and param.name == arg_key
)
for param in params
)
def arg_passable(fun: Callable[..., Any], arg_ix: int) -> bool:
assert callable(fun)
assert isinstance(arg_ix, int)
params: ValuesView[Parameter] = signature(fun).parameters.values()
return sum(
1
for param in params
if param.kind in [Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD]
) > arg_ix or any(param.kind is Parameter.VAR_POSITIONAL for param in params)
I want to test passable
on randomly generated dummy functions using Hypothesis.
You can assign a Signature
object to the callable’s .__signature__
attribute:
import inspect
from inspect import Signature as S, Parameter as P
def f(x: int = 2): pass
print(inspect.signature(f))
new_param = P(name="y", kind=P.KEYWORD_ONLY, default="abc", annotation=str)
f.__signature__ = S(parameters=[new_param])
print(inspect.signature(f))
(x: int = 2)
(*, y: str = 'abc')
Though note that of course this won’t affect what happens if you try to call it; f(y="def")
will give you a TypeError: f() got an unexpected keyword argument 'y'
, which you can in turn work around with *args, **kwargs
.