diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 10339641c3b..56c2f76708d 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -562,7 +562,7 @@ The Signature object represents the call signature of a callable object and its return annotation. To retrieve a Signature object, use the :func:`signature` function. -.. function:: signature(callable, *, follow_wrapped=True, globalns=None, localns=None) +.. function:: signature(callable, *, follow_wrapped=True, globals=None, locals=None, eval_str=False) Return a :class:`Signature` object for the given ``callable``:: @@ -584,11 +584,20 @@ function. Accepts a wide range of Python callables, from plain functions and classes to :func:`functools.partial` objects. - Raises :exc:`ValueError` if no signature can be provided, and - :exc:`TypeError` if that type of object is not supported. + For objects defined in modules using stringized annotations + (``from __future__ import annotations``), :func:`signature` will + attempt to automatically un-stringize the annotations using + :func:`inspect.get_annotations()`. The + ``global``, ``locals``, and ``eval_str`` parameters are passed + into :func:`inspect.get_annotations()` when resolving the + annotations; see the documentation for :func:`inspect.get_annotations()` + for instructions on how to use these parameters. - ``globalns`` and ``localns`` are passed into - :func:`typing.get_type_hints` when resolving the annotations. + Raises :exc:`ValueError` if no signature can be provided, and + :exc:`TypeError` if that type of object is not supported. Also, + if the annotations are stringized, and ``eval_str`` is not false, + the ``eval()`` call(s) to un-stringize the annotations could + potentially raise any kind of exception. A slash(/) in the signature of a function denotes that the parameters prior to it are positional-only. For more info, see @@ -600,7 +609,7 @@ function. unwrap decorated callables.) .. versionadded:: 3.10 - ``globalns`` and ``localns`` parameters. + ``globals``, ``locals``, and ``eval_str`` parameters. .. note:: @@ -608,12 +617,6 @@ function. Python. For example, in CPython, some built-in functions defined in C provide no metadata about their arguments. - .. note:: - - Will first try to resolve the annotations, but when it fails and - encounters with an error while that operation, the annotations will be - returned unchanged (as strings). - .. class:: Signature(parameters=None, *, return_annotation=Signature.empty) @@ -1115,6 +1118,55 @@ Classes and functions .. versionadded:: 3.4 +.. function:: get_annotations(obj, *, globals=None, locals=None, eval_str=False) + + Compute the annotations dict for an object. + + ``obj`` may be a callable, class, or module. + Passing in an object of any other type raises :exc:`TypeError`. + + Returns a dict. ``get_annotations()`` returns a new dict every time + it's called; calling it twice on the same object will return two + different but equivalent dicts. + + This function handles several details for you: + + * If ``eval_str`` is true, values of type ``str`` will + be un-stringized using :func:`eval()`. This is intended + for use with stringized annotations + (``from __future__ import annotations``). + * If ``obj`` doesn't have an annotations dict, returns an + empty dict. (Functions and methods always have an + annotations dict; classes, modules, and other types of + callables may not.) + * Ignores inherited annotations on classes. If a class + doesn't have its own annotations dict, returns an empty dict. + * All accesses to object members and dict values are done + using ``getattr()`` and ``dict.get()`` for safety. + * Always, always, always returns a freshly-created dict. + + ``eval_str`` controls whether or not values of type ``str`` are replaced + with the result of calling :func:`eval()` on those values: + + * If eval_str is true, :func:`eval()` is called on values of type ``str``. + * If eval_str is false (the default), values of type ``str`` are unchanged. + + ``globals`` and ``locals`` are passed in to :func:`eval()`; see the documentation + for :func:`eval()` for more information. If ``globals`` or ``locals`` + is ``None``, this function may replace that value with a context-specific + default, contingent on ``type(obj)``: + + * If ``obj`` is a module, ``globals`` defaults to ``obj.__dict__``. + * If ``obj`` is a class, ``globals`` defaults to + ``sys.modules[obj.__module__].__dict__`` and ``locals`` defaults + to the ``obj`` class namespace. + * If ``obj`` is a callable, ``globals`` defaults to ``obj.__globals__``, + although if ``obj`` is a wrapped function (using + ``functools.update_wrapper()``) it is first unwrapped. + + .. versionadded:: 3.10 + + .. _inspect-stack: The interpreter stack diff --git a/Lib/inspect.py b/Lib/inspect.py index b8e247ec252..9f8cc01310f 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -24,6 +24,8 @@ Here are some of the useful functions provided by this module: stack(), trace() - get info about frames on the stack or in a traceback signature() - get a Signature object for the callable + + get_annotations() - safely compute an object's annotations """ # This module is in the public domain. No warranties. @@ -60,6 +62,122 @@ for k, v in dis.COMPILER_FLAG_NAMES.items(): # See Include/object.h TPFLAGS_IS_ABSTRACT = 1 << 20 + +def get_annotations(obj, *, globals=None, locals=None, eval_str=False): + """Compute the annotations dict for an object. + + obj may be a callable, class, or module. + Passing in an object of any other type raises TypeError. + + Returns a dict. get_annotations() returns a new dict every time + it's called; calling it twice on the same object will return two + different but equivalent dicts. + + This function handles several details for you: + + * If eval_str is true, values of type str will + be un-stringized using eval(). This is intended + for use with stringized annotations + ("from __future__ import annotations"). + * If obj doesn't have an annotations dict, returns an + empty dict. (Functions and methods always have an + annotations dict; classes, modules, and other types of + callables may not.) + * Ignores inherited annotations on classes. If a class + doesn't have its own annotations dict, returns an empty dict. + * All accesses to object members and dict values are done + using getattr() and dict.get() for safety. + * Always, always, always returns a freshly-created dict. + + eval_str controls whether or not values of type str are replaced + with the result of calling eval() on those values: + + * If eval_str is true, eval() is called on values of type str. + * If eval_str is false (the default), values of type str are unchanged. + + globals and locals are passed in to eval(); see the documentation + for eval() for more information. If either globals or locals is + None, this function may replace that value with a context-specific + default, contingent on type(obj): + + * If obj is a module, globals defaults to obj.__dict__. + * If obj is a class, globals defaults to + sys.modules[obj.__module__].__dict__ and locals + defaults to the obj class namespace. + * If obj is a callable, globals defaults to obj.__globals__, + although if obj is a wrapped function (using + functools.update_wrapper()) it is first unwrapped. + """ + if isinstance(obj, type): + # class + obj_dict = getattr(obj, '__dict__', None) + if obj_dict and hasattr(obj_dict, 'get'): + ann = obj_dict.get('__annotations__', None) + if isinstance(ann, types.GetSetDescriptorType): + ann = None + else: + ann = None + + obj_globals = None + module_name = getattr(obj, '__module__', None) + if module_name: + module = sys.modules.get(module_name, None) + if module: + obj_globals = getattr(module, '__dict__', None) + obj_locals = dict(vars(obj)) + unwrap = obj + elif isinstance(obj, types.ModuleType): + # module + ann = getattr(obj, '__annotations__', None) + obj_globals = getattr(obj, '__dict__') + obj_locals = None + unwrap = None + elif callable(obj): + # this includes types.Function, types.BuiltinFunctionType, + # types.BuiltinMethodType, functools.partial, functools.singledispatch, + # "class funclike" from Lib/test/test_inspect... on and on it goes. + ann = getattr(obj, '__annotations__', None) + obj_globals = getattr(obj, '__globals__', None) + obj_locals = None + unwrap = obj + else: + raise TypeError(f"{obj!r} is not a module, class, or callable.") + + if ann is None: + return {} + + if not isinstance(ann, dict): + raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None") + + if not ann: + return {} + + if not eval_str: + return dict(ann) + + if unwrap is not None: + while True: + if hasattr(unwrap, '__wrapped__'): + unwrap = unwrap.__wrapped__ + continue + if isinstance(unwrap, functools.partial): + unwrap = unwrap.func + continue + break + if hasattr(unwrap, "__globals__"): + obj_globals = unwrap.__globals__ + + if globals is None: + globals = obj_globals + if locals is None: + locals = obj_locals + + return_value = {key: + value if not isinstance(value, str) else eval(value, globals, locals) + for key, value in ann.items() } + return return_value + + # ----------------------------------------------------------- type-checking def ismodule(object): """Return true if the object is a module. @@ -1165,7 +1283,8 @@ def getfullargspec(func): sig = _signature_from_callable(func, follow_wrapper_chains=False, skip_bound_arg=False, - sigcls=Signature) + sigcls=Signature, + eval_str=False) except Exception as ex: # Most of the times 'signature' will raise ValueError. # But, it can also raise AttributeError, and, maybe something @@ -1898,7 +2017,7 @@ def _signature_is_functionlike(obj): isinstance(name, str) and (defaults is None or isinstance(defaults, tuple)) and (kwdefaults is None or isinstance(kwdefaults, dict)) and - isinstance(annotations, dict)) + (isinstance(annotations, (dict)) or annotations is None) ) def _signature_get_bound_param(spec): @@ -2151,7 +2270,7 @@ def _signature_from_builtin(cls, func, skip_bound_arg=True): def _signature_from_function(cls, func, skip_bound_arg=True, - globalns=None, localns=None): + globals=None, locals=None, eval_str=False): """Private helper: constructs Signature for the given python function.""" is_duck_function = False @@ -2177,7 +2296,7 @@ def _signature_from_function(cls, func, skip_bound_arg=True, positional = arg_names[:pos_count] keyword_only_count = func_code.co_kwonlyargcount keyword_only = arg_names[pos_count:pos_count + keyword_only_count] - annotations = func.__annotations__ + annotations = get_annotations(func, globals=globals, locals=locals, eval_str=eval_str) defaults = func.__defaults__ kwdefaults = func.__kwdefaults__ @@ -2248,8 +2367,9 @@ def _signature_from_function(cls, func, skip_bound_arg=True, def _signature_from_callable(obj, *, follow_wrapper_chains=True, skip_bound_arg=True, - globalns=None, - localns=None, + globals=None, + locals=None, + eval_str=False, sigcls): """Private helper function to get signature for arbitrary @@ -2259,9 +2379,10 @@ def _signature_from_callable(obj, *, _get_signature_of = functools.partial(_signature_from_callable, follow_wrapper_chains=follow_wrapper_chains, skip_bound_arg=skip_bound_arg, - globalns=globalns, - localns=localns, - sigcls=sigcls) + globals=globals, + locals=locals, + sigcls=sigcls, + eval_str=eval_str) if not callable(obj): raise TypeError('{!r} is not a callable object'.format(obj)) @@ -2330,7 +2451,7 @@ def _signature_from_callable(obj, *, # of a Python function (Cython functions, for instance), then: return _signature_from_function(sigcls, obj, skip_bound_arg=skip_bound_arg, - globalns=globalns, localns=localns) + globals=globals, locals=locals, eval_str=eval_str) if _signature_is_builtin(obj): return _signature_from_builtin(sigcls, obj, @@ -2854,11 +2975,11 @@ class Signature: @classmethod def from_callable(cls, obj, *, - follow_wrapped=True, globalns=None, localns=None): + follow_wrapped=True, globals=None, locals=None, eval_str=False): """Constructs Signature for the given callable object.""" return _signature_from_callable(obj, sigcls=cls, follow_wrapper_chains=follow_wrapped, - globalns=globalns, localns=localns) + globals=globals, locals=locals, eval_str=eval_str) @property def parameters(self): @@ -3106,10 +3227,10 @@ class Signature: return rendered -def signature(obj, *, follow_wrapped=True, globalns=None, localns=None): +def signature(obj, *, follow_wrapped=True, globals=None, locals=None, eval_str=False): """Get a signature object for the passed callable.""" return Signature.from_callable(obj, follow_wrapped=follow_wrapped, - globalns=globalns, localns=localns) + globals=globals, locals=locals, eval_str=eval_str) def _main(): diff --git a/Lib/test/inspect_stock_annotations.py b/Lib/test/inspect_stock_annotations.py new file mode 100644 index 00000000000..d115a25b650 --- /dev/null +++ b/Lib/test/inspect_stock_annotations.py @@ -0,0 +1,28 @@ +a:int=3 +b:str="foo" + +class MyClass: + a:int=4 + b:str="bar" + def __init__(self, a, b): + self.a = a + self.b = b + def __eq__(self, other): + return isinstance(other, MyClass) and self.a == other.a and self.b == other.b + +def function(a:int, b:str) -> MyClass: + return MyClass(a, b) + + +def function2(a:int, b:"str", c:MyClass) -> MyClass: + pass + + +def function3(a:"int", b:"str", c:"MyClass"): + pass + + +class UnannotatedClass: + pass + +def unannotated_function(a, b, c): pass diff --git a/Lib/test/inspect_stringized_annotations.py b/Lib/test/inspect_stringized_annotations.py new file mode 100644 index 00000000000..a56fb050ead --- /dev/null +++ b/Lib/test/inspect_stringized_annotations.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +a:int=3 +b:str="foo" + +class MyClass: + a:int=4 + b:str="bar" + def __init__(self, a, b): + self.a = a + self.b = b + def __eq__(self, other): + return isinstance(other, MyClass) and self.a == other.a and self.b == other.b + +def function(a:int, b:str) -> MyClass: + return MyClass(a, b) + + +def function2(a:int, b:"str", c:MyClass) -> MyClass: + pass + + +def function3(a:"int", b:"str", c:"MyClass"): + pass + + +class UnannotatedClass: + pass + +def unannotated_function(a, b, c): pass + +class MyClassWithLocalAnnotations: + mytype = int + x: mytype diff --git a/Lib/test/inspect_stringized_annotations_2.py b/Lib/test/inspect_stringized_annotations_2.py new file mode 100644 index 00000000000..87206d5a646 --- /dev/null +++ b/Lib/test/inspect_stringized_annotations_2.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +def foo(a, b, c): pass diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index b32b3d37577..0ab65307cdd 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -32,6 +32,9 @@ from test.support.script_helper import assert_python_ok, assert_python_failure from test import inspect_fodder as mod from test import inspect_fodder2 as mod2 from test import support +from test import inspect_stock_annotations +from test import inspect_stringized_annotations +from test import inspect_stringized_annotations_2 from test.test_import import _ready_to_import @@ -1281,6 +1284,106 @@ class TestClassesAndFunctions(unittest.TestCase): attrs = [a[0] for a in inspect.getmembers(C)] self.assertNotIn('missing', attrs) + def test_get_annotations_with_stock_annotations(self): + def foo(a:int, b:str): pass + self.assertEqual(inspect.get_annotations(foo), {'a': int, 'b': str}) + + foo.__annotations__ = {'a': 'foo', 'b':'str'} + self.assertEqual(inspect.get_annotations(foo), {'a': 'foo', 'b': 'str'}) + + self.assertEqual(inspect.get_annotations(foo, eval_str=True, locals=locals()), {'a': foo, 'b': str}) + self.assertEqual(inspect.get_annotations(foo, eval_str=True, globals=locals()), {'a': foo, 'b': str}) + + isa = inspect_stock_annotations + self.assertEqual(inspect.get_annotations(isa), {'a': int, 'b': str}) + self.assertEqual(inspect.get_annotations(isa.MyClass), {'a': int, 'b': str}) + self.assertEqual(inspect.get_annotations(isa.function), {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual(inspect.get_annotations(isa.function2), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass}) + self.assertEqual(inspect.get_annotations(isa.function3), {'a': 'int', 'b': 'str', 'c': 'MyClass'}) + self.assertEqual(inspect.get_annotations(inspect), {}) # inspect module has no annotations + self.assertEqual(inspect.get_annotations(isa.UnannotatedClass), {}) + self.assertEqual(inspect.get_annotations(isa.unannotated_function), {}) + + self.assertEqual(inspect.get_annotations(isa, eval_str=True), {'a': int, 'b': str}) + self.assertEqual(inspect.get_annotations(isa.MyClass, eval_str=True), {'a': int, 'b': str}) + self.assertEqual(inspect.get_annotations(isa.function, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual(inspect.get_annotations(isa.function2, eval_str=True), {'a': int, 'b': str, 'c': isa.MyClass, 'return': isa.MyClass}) + self.assertEqual(inspect.get_annotations(isa.function3, eval_str=True), {'a': int, 'b': str, 'c': isa.MyClass}) + self.assertEqual(inspect.get_annotations(inspect, eval_str=True), {}) + self.assertEqual(inspect.get_annotations(isa.UnannotatedClass, eval_str=True), {}) + self.assertEqual(inspect.get_annotations(isa.unannotated_function, eval_str=True), {}) + + self.assertEqual(inspect.get_annotations(isa, eval_str=False), {'a': int, 'b': str}) + self.assertEqual(inspect.get_annotations(isa.MyClass, eval_str=False), {'a': int, 'b': str}) + self.assertEqual(inspect.get_annotations(isa.function, eval_str=False), {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual(inspect.get_annotations(isa.function2, eval_str=False), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass}) + self.assertEqual(inspect.get_annotations(isa.function3, eval_str=False), {'a': 'int', 'b': 'str', 'c': 'MyClass'}) + self.assertEqual(inspect.get_annotations(inspect, eval_str=False), {}) + self.assertEqual(inspect.get_annotations(isa.UnannotatedClass, eval_str=False), {}) + self.assertEqual(inspect.get_annotations(isa.unannotated_function, eval_str=False), {}) + + def times_three(fn): + @functools.wraps(fn) + def wrapper(a, b): + return fn(a*3, b*3) + return wrapper + + wrapped = times_three(isa.function) + self.assertEqual(wrapped(1, 'x'), isa.MyClass(3, 'xxx')) + self.assertIsNot(wrapped.__globals__, isa.function.__globals__) + self.assertEqual(inspect.get_annotations(wrapped), {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual(inspect.get_annotations(wrapped, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual(inspect.get_annotations(wrapped, eval_str=False), {'a': int, 'b': str, 'return': isa.MyClass}) + + def test_get_annotations_with_stringized_annotations(self): + isa = inspect_stringized_annotations + self.assertEqual(inspect.get_annotations(isa), {'a': 'int', 'b': 'str'}) + self.assertEqual(inspect.get_annotations(isa.MyClass), {'a': 'int', 'b': 'str'}) + self.assertEqual(inspect.get_annotations(isa.function), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) + self.assertEqual(inspect.get_annotations(isa.function2), {'a': 'int', 'b': "'str'", 'c': 'MyClass', 'return': 'MyClass'}) + self.assertEqual(inspect.get_annotations(isa.function3), {'a': "'int'", 'b': "'str'", 'c': "'MyClass'"}) + self.assertEqual(inspect.get_annotations(isa.UnannotatedClass), {}) + self.assertEqual(inspect.get_annotations(isa.unannotated_function), {}) + + self.assertEqual(inspect.get_annotations(isa, eval_str=True), {'a': int, 'b': str}) + self.assertEqual(inspect.get_annotations(isa.MyClass, eval_str=True), {'a': int, 'b': str}) + self.assertEqual(inspect.get_annotations(isa.function, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual(inspect.get_annotations(isa.function2, eval_str=True), {'a': int, 'b': 'str', 'c': isa.MyClass, 'return': isa.MyClass}) + self.assertEqual(inspect.get_annotations(isa.function3, eval_str=True), {'a': 'int', 'b': 'str', 'c': 'MyClass'}) + self.assertEqual(inspect.get_annotations(isa.UnannotatedClass, eval_str=True), {}) + self.assertEqual(inspect.get_annotations(isa.unannotated_function, eval_str=True), {}) + + self.assertEqual(inspect.get_annotations(isa, eval_str=False), {'a': 'int', 'b': 'str'}) + self.assertEqual(inspect.get_annotations(isa.MyClass, eval_str=False), {'a': 'int', 'b': 'str'}) + self.assertEqual(inspect.get_annotations(isa.function, eval_str=False), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) + self.assertEqual(inspect.get_annotations(isa.function2, eval_str=False), {'a': 'int', 'b': "'str'", 'c': 'MyClass', 'return': 'MyClass'}) + self.assertEqual(inspect.get_annotations(isa.function3, eval_str=False), {'a': "'int'", 'b': "'str'", 'c': "'MyClass'"}) + self.assertEqual(inspect.get_annotations(isa.UnannotatedClass, eval_str=False), {}) + self.assertEqual(inspect.get_annotations(isa.unannotated_function, eval_str=False), {}) + + isa2 = inspect_stringized_annotations_2 + self.assertEqual(inspect.get_annotations(isa2), {}) + self.assertEqual(inspect.get_annotations(isa2, eval_str=True), {}) + self.assertEqual(inspect.get_annotations(isa2, eval_str=False), {}) + + def times_three(fn): + @functools.wraps(fn) + def wrapper(a, b): + return fn(a*3, b*3) + return wrapper + + wrapped = times_three(isa.function) + self.assertEqual(wrapped(1, 'x'), isa.MyClass(3, 'xxx')) + self.assertIsNot(wrapped.__globals__, isa.function.__globals__) + self.assertEqual(inspect.get_annotations(wrapped), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) + self.assertEqual(inspect.get_annotations(wrapped, eval_str=True), {'a': int, 'b': str, 'return': isa.MyClass}) + self.assertEqual(inspect.get_annotations(wrapped, eval_str=False), {'a': 'int', 'b': 'str', 'return': 'MyClass'}) + + # test that local namespace lookups work + self.assertEqual(inspect.get_annotations(isa.MyClassWithLocalAnnotations), {'x': 'mytype'}) + self.assertEqual(inspect.get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), {'x': int}) + + class TestIsDataDescriptor(unittest.TestCase): def test_custom_descriptors(self): @@ -2786,13 +2889,13 @@ class TestSignatureObject(unittest.TestCase): pass ham = partialmethod(test, c=1) - self.assertEqual(self.signature(Spam.ham), + self.assertEqual(self.signature(Spam.ham, eval_str=False), ((('it', ..., ..., 'positional_or_keyword'), ('a', ..., ..., 'positional_or_keyword'), ('c', 1, ..., 'keyword_only')), 'spam')) - self.assertEqual(self.signature(Spam().ham), + self.assertEqual(self.signature(Spam().ham, eval_str=False), ((('a', ..., ..., 'positional_or_keyword'), ('c', 1, ..., 'keyword_only')), 'spam')) @@ -2803,7 +2906,7 @@ class TestSignatureObject(unittest.TestCase): g = partialmethod(test, 1) - self.assertEqual(self.signature(Spam.g), + self.assertEqual(self.signature(Spam.g, eval_str=False), ((('self', ..., 'anno', 'positional_or_keyword'),), ...)) @@ -3265,15 +3368,145 @@ class TestSignatureObject(unittest.TestCase): self.assertEqual(sig1.return_annotation, int) self.assertEqual(sig1.parameters['foo'].annotation, Foo) - sig2 = signature_func(func, localns=locals()) + sig2 = signature_func(func, locals=locals()) self.assertEqual(sig2.return_annotation, int) self.assertEqual(sig2.parameters['foo'].annotation, Foo) - sig3 = signature_func(func2, globalns={'Bar': int}, localns=locals()) + sig3 = signature_func(func2, globals={'Bar': int}, locals=locals()) self.assertEqual(sig3.return_annotation, int) self.assertEqual(sig3.parameters['foo'].annotation, Foo) self.assertEqual(sig3.parameters['bar'].annotation, 'Bar') + def test_signature_eval_str(self): + isa = inspect_stringized_annotations + sig = inspect.Signature + par = inspect.Parameter + PORK = inspect.Parameter.POSITIONAL_OR_KEYWORD + for signature_func in (inspect.signature, inspect.Signature.from_callable): + with self.subTest(signature_func = signature_func): + self.assertEqual( + signature_func(isa.MyClass), + sig( + parameters=( + par('a', PORK), + par('b', PORK), + ))) + self.assertEqual( + signature_func(isa.function), + sig( + return_annotation='MyClass', + parameters=( + par('a', PORK, annotation='int'), + par('b', PORK, annotation='str'), + ))) + self.assertEqual( + signature_func(isa.function2), + sig( + return_annotation='MyClass', + parameters=( + par('a', PORK, annotation='int'), + par('b', PORK, annotation="'str'"), + par('c', PORK, annotation="MyClass"), + ))) + self.assertEqual( + signature_func(isa.function3), + sig( + parameters=( + par('a', PORK, annotation="'int'"), + par('b', PORK, annotation="'str'"), + par('c', PORK, annotation="'MyClass'"), + ))) + + self.assertEqual(signature_func(isa.UnannotatedClass), sig()) + self.assertEqual(signature_func(isa.unannotated_function), + sig( + parameters=( + par('a', PORK), + par('b', PORK), + par('c', PORK), + ))) + + self.assertEqual( + signature_func(isa.MyClass, eval_str=True), + sig( + parameters=( + par('a', PORK), + par('b', PORK), + ))) + self.assertEqual( + signature_func(isa.function, eval_str=True), + sig( + return_annotation=isa.MyClass, + parameters=( + par('a', PORK, annotation=int), + par('b', PORK, annotation=str), + ))) + self.assertEqual( + signature_func(isa.function2, eval_str=True), + sig( + return_annotation=isa.MyClass, + parameters=( + par('a', PORK, annotation=int), + par('b', PORK, annotation='str'), + par('c', PORK, annotation=isa.MyClass), + ))) + self.assertEqual( + signature_func(isa.function3, eval_str=True), + sig( + parameters=( + par('a', PORK, annotation='int'), + par('b', PORK, annotation='str'), + par('c', PORK, annotation='MyClass'), + ))) + + globalns = {'int': float, 'str': complex} + localns = {'str': tuple, 'MyClass': dict} + with self.assertRaises(NameError): + signature_func(isa.function, eval_str=True, globals=globalns) + + self.assertEqual( + signature_func(isa.function, eval_str=True, locals=localns), + sig( + return_annotation=dict, + parameters=( + par('a', PORK, annotation=int), + par('b', PORK, annotation=tuple), + ))) + + self.assertEqual( + signature_func(isa.function, eval_str=True, globals=globalns, locals=localns), + sig( + return_annotation=dict, + parameters=( + par('a', PORK, annotation=float), + par('b', PORK, annotation=tuple), + ))) + + def test_signature_none_annotation(self): + class funclike: + # Has to be callable, and have correct + # __code__, __annotations__, __defaults__, __name__, + # and __kwdefaults__ attributes + + def __init__(self, func): + self.__name__ = func.__name__ + self.__code__ = func.__code__ + self.__annotations__ = func.__annotations__ + self.__defaults__ = func.__defaults__ + self.__kwdefaults__ = func.__kwdefaults__ + self.func = func + + def __call__(self, *args, **kwargs): + return self.func(*args, **kwargs) + + def foo(): pass + foo = funclike(foo) + foo.__annotations__ = None + for signature_func in (inspect.signature, inspect.Signature.from_callable): + with self.subTest(signature_func = signature_func): + self.assertEqual(signature_func(foo), inspect.Signature()) + self.assertEqual(inspect.get_annotations(foo), {}) + class TestParameterObject(unittest.TestCase): def test_signature_parameter_kinds(self): diff --git a/Misc/NEWS.d/next/Library/2021-04-22-04-12-13.bpo-43817.FQ-XlH.rst b/Misc/NEWS.d/next/Library/2021-04-22-04-12-13.bpo-43817.FQ-XlH.rst new file mode 100644 index 00000000000..36a6018bab2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-04-22-04-12-13.bpo-43817.FQ-XlH.rst @@ -0,0 +1,11 @@ +Add :func:`inspect.get_annotations`, which safely computes the annotations +defined on an object. It works around the quirks of accessing the +annotations from various types of objects, and makes very few assumptions +about the object passed in. :func:`inspect.get_annotations` can also +correctly un-stringize stringized annotations. + +:func:`inspect.signature`, :func:`inspect.from_callable`, and +:func:`inspect.from_function` now call :func:`inspect.get_annotations` +to retrieve annotations. This means :func:`inspect.signature` +and :func:`inspect.from_callable` can now un-stringize stringized +annotations, too.