1

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.

schuelermine
  • 1,958
  • 2
  • 17
  • 31
  • Do you specifically need the functions, or just the `Signature` objects? If your code expects a function so that it can call `inspect.signature`, you could change it to accept a `Signature` object directly, and then have a separate function which accepts a function but just calls `inspect.signature` and your other function. – kaya3 Nov 21 '22 at 23:51
  • It seems like it'd make the most sense to just define the functions you need with ordinary `def` statements instead of trying to do anything fancy. – user2357112 Nov 21 '22 at 23:54
  • can you please provide more a concrete example of what you are doing? It may not be strictly necessary in this case but it helps a question gain traction – juanpa.arrivillaga Nov 22 '22 at 00:10
  • @juanpa.arrivillaga I have done so – schuelermine Nov 22 '22 at 00:34
  • Why do you want to avoid ```eval()```? How will you generate a function body? Anyway, there's a hard way to use ```ast.Module```, ```ast.FunctionDef```, ```compile()```, ```exec()```, etc. – relent95 Nov 22 '22 at 05:43

1 Answers1

1

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.

Zac Hatfield-Dodds
  • 2,455
  • 6
  • 19