[Python-checkins] bpo-43817: Add inspect.get_annotations(). (#25522)

larryhastings webhook-mailer at python.org
Fri Apr 30 00:16:36 EDT 2021


https://github.com/python/cpython/commit/74613a46fc79cacc88d3eae4105b12691cd4ba20
commit: 74613a46fc79cacc88d3eae4105b12691cd4ba20
branch: master
author: larryhastings <larry at hastings.org>
committer: larryhastings <larry at hastings.org>
date: 2021-04-29T21:16:28-07:00
summary:

bpo-43817: Add inspect.get_annotations(). (#25522)

Add 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. inspect.get_annotations can also correctly un-stringize stringized annotations.

inspect.signature, inspect.from_callable, and inspect.from_function now call inspect.get_annotations to retrieve annotations.  This means inspect.signature and inspect.from_callable can now un-stringize stringized annotations, too.

files:
A Lib/test/inspect_stock_annotations.py
A Lib/test/inspect_stringized_annotations.py
A Lib/test/inspect_stringized_annotations_2.py
A Misc/NEWS.d/next/Library/2021-04-22-04-12-13.bpo-43817.FQ-XlH.rst
M Doc/library/inspect.rst
M Lib/inspect.py
M Lib/test/test_inspect.py

diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst
index 10339641c3b43..56c2f76708d43 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 b8e247ec252bb..9f8cc01310f10 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -24,6 +24,8 @@
     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 @@
 # 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 @@ def from_builtin(cls, func):
 
     @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 @@ def __str__(self):
         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 0000000000000..d115a25b65034
--- /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 0000000000000..a56fb050ead97
--- /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 0000000000000..87206d5a64692
--- /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 b32b3d3757750..0ab65307cdd09 100644
--- a/Lib/test/test_inspect.py
+++ b/Lib/test/test_inspect.py
@@ -32,6 +32,9 @@
 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 C(metaclass=M):
         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 @@ def test(it, a, *, c) -> 'spam':
                 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 @@ def test(self: 'anno', x):
 
             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 @@ def func2(foo: Foo, bar: 'Bar') -> int: pass
                 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 0000000000000..36a6018bab253
--- /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.



More information about the Python-checkins mailing list