Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

inspect.signature with PEP 563

The following code:

import inspect
from typing import NamedTuple

class Example(NamedTuple):
    a: str

if __name__== "__main__":
    signature: inspect.Signature = inspect.signature(Example)
    print(signature)

outputs:

(a: str)

However when enabling PEP 563 – Postponed Evaluation of Annotations:

from __future__ import annotations
import inspect
from typing import NamedTuple

class Example(NamedTuple):
    a: str

if __name__== "__main__":
    signature: inspect.Signature = inspect.signature(Example)
    print(signature)

The output is:

(a: 'str')

How can I get the exact same object of type inspect.Signature with PEP 563 like without it?

like image 528
Tobias Hermann Avatar asked Nov 20 '18 10:11

Tobias Hermann


2 Answers

The point of using PEP 536 is to not evaluate the annotations unless needed. The signature merely reports on the annotations.

If for your purposes you need to have the annotations resolved, you have to do so yourself. PEP 536 tells documents how you do this:

For code that uses type hints, the typing.get_type_hints(obj, globalns=None, localns=None) function correctly evaluates expressions back from its string form.

[...]

For code which uses annotations for other purposes, a regular eval(ann, globals, locals) call is enough to resolve the annotation.

You could even use the typing.get_type_hints() function to assign back to __annotations__ before getting the signature:

import typing

Example.__new__.__annotations__ = typing.get_type_hints(Example.__new__)
signature: inspect.Signature = inspect.signature(Example)

Doing this is safe even if from __future__ import annotations had not been used.

like image 184
Martijn Pieters Avatar answered Sep 22 '22 13:09

Martijn Pieters


You have to actually use eval to get the same behaviour:

from __future__ import annotations
import inspect
from typing import NamedTuple

class Example(NamedTuple):
    a: str

signature: inspect.Signature = inspect.signature(Example)
print(signature)

# extra bit
globalns = getattr(Example, '__globals__', {})
for param in list(signature.parameters.values()):
  if isinstance(param.annotation, str):
    param._annotation = eval(param.annotation, globalns)

print(signature)

You will get:

(a: 'str')
(a: str)

Alternatively you can modify __annotations__ before calling inspect.signature(obj), but I am finding it too hard, because I need to cover multiple different cases.

Answer by @Martijn Pieters misses one detail about typing.get_type_hints:

if necessary adds Optional[t] if a default value equal to None is set

Example:

# without imporing annotations from __future__
import inspect
import typing

def func(a: str=None): pass
print(inspect.signature(func))
func.__annotations__ = typing.get_type_hints(func)
print(inspect.signature(func))

You will get:

(a: str = None)
(a: Union[str, NoneType] = None)
like image 34
m4js7er Avatar answered Sep 21 '22 13:09

m4js7er