6

I am working within a Python web framework that uses Python 3 type annotations for validation and dependency injection.

So I am looking for a way to generate functions with type annotations from a parameters given to the generating function:

def gen_fn(args: Dict[str, Any]) -> Callable:
    def new_fn(???):
        pass
    return new_fn

so that

inspect.signature(gen_fn({'a': int}))

will return

<Signature (a:int)>

Is there something I cam put instead of the ??? that will do the thing I need.

I also looked at Signature.replace() in the inspect module, but did not find a way to attach the new signature to a new or existing function.

I am hesitant to use ast because:

The abstract syntax itself might change with each Python release

So my question is: What (if any) is a reasonable way to generate a function with Python 3 type annotation based on a dict passed to the generating function?


Edit: while @Aran-Fey's solution answer my question correctly, it appears that my assumption was wrong. Changing the signature doesn't allow calling the new_fn using the new signature. That is gen_fn({'a': int})(a=42) raises a TypeError: ... `got an unexpected keyword argument 'a'.

Chen Levy
  • 15,438
  • 17
  • 74
  • 92
  • 1
    I guess one way to achieve that is to build a string and `eval()` the whole string to get the function definition. But I don't think that's the best way to do it. – thuyein May 17 '18 at 06:40
  • [MonkeyType](https://github.com/Instagram/MonkeyType) can generate type annotations for some functions, but it requires the types to be checked at run-time. – Anderson Green Dec 03 '20 at 23:14

1 Answers1

12

Instead of creating a function with annotations, it's easier to create a function and then set the annotations manually.

  • inspect.signature looks for the existence of a __signature__ attribute before it looks at the function's actual signature, so we can craft an appropriate inspect.Signature object and assign it there:

    params = [inspect.Parameter(param,
                                inspect.Parameter.POSITIONAL_OR_KEYWORD,
                                annotation=type_)
                            for param, type_ in args.items()]
    new_fn.__signature__ = inspect.Signature(params)
    
  • typing.get_type_hints does not respect __signature__, so we should update the __annotations__ attribute as well:

    new_fn.__annotations__ = args
    

Putting them both together:

def gen_fn(args: Dict[str, Any]) -> Callable:
    def new_fn():
        pass

    params = [inspect.Parameter(param,
                                inspect.Parameter.POSITIONAL_OR_KEYWORD,
                                annotation=type_)
                            for param, type_ in args.items()]
    new_fn.__signature__ = inspect.Signature(params)
    new_fn.__annotations__ = args

    return new_fn

print(inspect.signature(gen_fn({'a': int})))  # (a:int)
print(get_type_hints(gen_fn({'a': int})))  # {'a': <class 'int'>}

Note that this doesn't make your function callable with these arguments; all of this is just smoke and mirrors that makes the function look like it has those parameters and annotations. Implementing the function is a separate issue.

You can define the function with varargs to aggregate all the arguments into a tuple and a dict:

def new_fn(*args, **kwargs):
    ...

But that still leaves you with the problem of implementing the function body. You haven't said what the function should do when it's called, so I can't help you with that. You can look at this question for some pointers.

Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
  • This seems to be correct but insufficient for my needs. See `Edit` to the OP. – Chen Levy May 21 '18 at 10:10
  • 1
    @ChenLevy Yes, implementing the function is a separate issue. You can make the function accept arbitrary arguments if you define it as `def new_fn(*args, **kwargs):`, but you still have to implement the function's body. You haven't specified what the function is supposed to do when it's called, so I can't give you much advice. Depending on the circumstances, you may even have to resort the writing code at runtime and then [`exec`](https://docs.python.org/3/library/functions.html#exec)ing it. – Aran-Fey May 21 '18 at 10:32
  • You made my day, sir! – sanzoghenzo Jul 21 '20 at 13:08