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.

Asked By: schuelermine

||

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.

Answered By: Zac Hatfield-Dodds
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.