summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/inspect.rst76
-rw-r--r--Lib/inspect.py149
-rw-r--r--Lib/test/inspect_stock_annotations.py28
-rw-r--r--Lib/test/inspect_stringized_annotations.py34
-rw-r--r--Lib/test/inspect_stringized_annotations_2.py3
-rw-r--r--Lib/test/test_inspect.py243
-rw-r--r--Misc/NEWS.d/next/Library/2021-04-22-04-12-13.bpo-43817.FQ-XlH.rst11
7 files changed, 513 insertions, 31 deletions
diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst
index 1033964..56c2f767 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 b8e247e..9f8cc01 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 0000000..d115a25
--- /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 0000000..a56fb05
--- /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 0000000..87206d5
--- /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 b32b3d3..0ab6530 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 0000000..36a6018
--- /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.