Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python metaprogramming: generate a function signature with type annotation

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'.

like image 316
Chen Levy Avatar asked Mar 07 '23 04:03

Chen Levy


1 Answers

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.

like image 121
Aran-Fey Avatar answered Mar 22 '23 00:03

Aran-Fey