diff options
author | larryhastings <larry@hastings.org> | 2021-04-30 04:16:28 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-30 04:16:28 (GMT) |
commit | 74613a46fc79cacc88d3eae4105b12691cd4ba20 (patch) | |
tree | e4bb45c84127a124ac969aa06e0946798a7e5bba /Lib/test/test_inspect.py | |
parent | a62e424de0c394cda178a8d934d06f0559b5e28d (diff) | |
download | cpython-74613a46fc79cacc88d3eae4105b12691cd4ba20.zip cpython-74613a46fc79cacc88d3eae4105b12691cd4ba20.tar.gz cpython-74613a46fc79cacc88d3eae4105b12691cd4ba20.tar.bz2 |
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.py | 243 |
1 files changed, 238 insertions, 5 deletions
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): |