summaryrefslogtreecommitdiff
path: root/Lib/test/test_inspect.py
diff options
context:
space:
mode:
authorlarryhastings <larry@hastings.org>2021-04-29 21:16:28 -0700
committerGitHub <noreply@github.com>2021-04-29 21:16:28 -0700
commit74613a46fc79cacc88d3eae4105b12691cd4ba20 (patch)
treee4bb45c84127a124ac969aa06e0946798a7e5bba /Lib/test/test_inspect.py
parenta62e424de0c394cda178a8d934d06f0559b5e28d (diff)
downloadcpython-git-74613a46fc79cacc88d3eae4105b12691cd4ba20.tar.gz
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.
Diffstat (limited to 'Lib/test/test_inspect.py')
-rw-r--r--Lib/test/test_inspect.py243
1 files changed, 238 insertions, 5 deletions
diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py
index b32b3d3757..0ab65307cd 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):