| """Tests for the annotations module.""" | |
| import textwrap | |
| import annotationlib | |
| import builtins | |
| import collections | |
| import functools | |
| import itertools | |
| import pickle | |
| from string.templatelib import Template, Interpolation | |
| import types | |
| import typing | |
| import sys | |
| import unittest | |
| from annotationlib import ( | |
| Format, | |
| ForwardRef, | |
| get_annotations, | |
| annotations_to_string, | |
| type_repr, | |
| ) | |
| from typing import Unpack, get_type_hints, List, Union | |
| from test import support | |
| from test.support import import_helper | |
| from test.test_inspect import inspect_stock_annotations | |
| from test.test_inspect import inspect_stringized_annotations | |
| from test.test_inspect import inspect_stringized_annotations_2 | |
| from test.test_inspect import inspect_stringized_annotations_pep695 | |
| def times_three(fn): | |
| @functools.wraps(fn) | |
| def wrapper(a, b): | |
| return fn(a * 3, b * 3) | |
| return wrapper | |
| class MyClass: | |
| def __repr__(self): | |
| return "my repr" | |
| class TestFormat(unittest.TestCase): | |
| def test_enum(self): | |
| self.assertEqual(Format.VALUE.value, 1) | |
| self.assertEqual(Format.VALUE, 1) | |
| self.assertEqual(Format.VALUE_WITH_FAKE_GLOBALS.value, 2) | |
| self.assertEqual(Format.VALUE_WITH_FAKE_GLOBALS, 2) | |
| self.assertEqual(Format.FORWARDREF.value, 3) | |
| self.assertEqual(Format.FORWARDREF, 3) | |
| self.assertEqual(Format.STRING.value, 4) | |
| self.assertEqual(Format.STRING, 4) | |
| class TestForwardRefFormat(unittest.TestCase): | |
| def test_closure(self): | |
| def inner(arg: x): | |
| pass | |
| anno = get_annotations(inner, format=Format.FORWARDREF) | |
| fwdref = anno["arg"] | |
| self.assertIsInstance(fwdref, ForwardRef) | |
| self.assertEqual(fwdref.__forward_arg__, "x") | |
| with self.assertRaises(NameError): | |
| fwdref.evaluate() | |
| x = 1 | |
| self.assertEqual(fwdref.evaluate(), x) | |
| anno = get_annotations(inner, format=Format.FORWARDREF) | |
| self.assertEqual(anno["arg"], x) | |
| def test_multiple_closure(self): | |
| def inner(arg: x[y]): | |
| pass | |
| fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] | |
| self.assertIsInstance(fwdref, ForwardRef) | |
| self.assertEqual(fwdref.__forward_arg__, "x[y]") | |
| with self.assertRaises(NameError): | |
| fwdref.evaluate() | |
| y = str | |
| fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] | |
| self.assertIsInstance(fwdref, ForwardRef) | |
| extra_name, extra_val = next(iter(fwdref.__extra_names__.items())) | |
| self.assertEqual(fwdref.__forward_arg__.replace(extra_name, extra_val.__name__), "x[str]") | |
| with self.assertRaises(NameError): | |
| fwdref.evaluate() | |
| x = list | |
| self.assertEqual(fwdref.evaluate(), x[y]) | |
| fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"] | |
| self.assertEqual(fwdref, x[y]) | |
| def test_function(self): | |
| def f(x: int, y: doesntexist): | |
| pass | |
| anno = get_annotations(f, format=Format.FORWARDREF) | |
| self.assertIs(anno["x"], int) | |
| fwdref = anno["y"] | |
| self.assertIsInstance(fwdref, ForwardRef) | |
| self.assertEqual(fwdref.__forward_arg__, "doesntexist") | |
| with self.assertRaises(NameError): | |
| fwdref.evaluate() | |
| self.assertEqual(fwdref.evaluate(globals={"doesntexist": 1}), 1) | |
| def test_nonexistent_attribute(self): | |
| def f( | |
| x: some.module, | |
| y: some[module], | |
| z: some(module), | |
| alpha: some | obj, | |
| beta: +some, | |
| gamma: some < obj, | |
| delta: some | {obj: module}, | |
| epsilon: some | {obj}, | |
| zeta: some | [obj, module], | |
| eta: some | (), | |
| ): | |
| pass | |
| anno = get_annotations(f, format=Format.FORWARDREF) | |
| x_anno = anno["x"] | |
| self.assertIsInstance(x_anno, ForwardRef) | |
| self.assertEqual(x_anno, support.EqualToForwardRef("some.module", owner=f)) | |
| y_anno = anno["y"] | |
| self.assertIsInstance(y_anno, ForwardRef) | |
| self.assertEqual(y_anno, support.EqualToForwardRef("some[module]", owner=f)) | |
| z_anno = anno["z"] | |
| self.assertIsInstance(z_anno, ForwardRef) | |
| self.assertEqual(z_anno, support.EqualToForwardRef("some(module)", owner=f)) | |
| alpha_anno = anno["alpha"] | |
| self.assertIsInstance(alpha_anno, ForwardRef) | |
| self.assertEqual(alpha_anno, support.EqualToForwardRef("some | obj", owner=f)) | |
| beta_anno = anno["beta"] | |
| self.assertIsInstance(beta_anno, ForwardRef) | |
| self.assertEqual(beta_anno, support.EqualToForwardRef("+some", owner=f)) | |
| gamma_anno = anno["gamma"] | |
| self.assertIsInstance(gamma_anno, ForwardRef) | |
| self.assertEqual(gamma_anno, support.EqualToForwardRef("some < obj", owner=f)) | |
| delta_anno = anno["delta"] | |
| self.assertIsInstance(delta_anno, ForwardRef) | |
| self.assertEqual(delta_anno, support.EqualToForwardRef("some | {obj: module}", owner=f)) | |
| epsilon_anno = anno["epsilon"] | |
| self.assertIsInstance(epsilon_anno, ForwardRef) | |
| self.assertEqual(epsilon_anno, support.EqualToForwardRef("some | {obj}", owner=f)) | |
| zeta_anno = anno["zeta"] | |
| self.assertIsInstance(zeta_anno, ForwardRef) | |
| self.assertEqual(zeta_anno, support.EqualToForwardRef("some | [obj, module]", owner=f)) | |
| eta_anno = anno["eta"] | |
| self.assertIsInstance(eta_anno, ForwardRef) | |
| self.assertEqual(eta_anno, support.EqualToForwardRef("some | ()", owner=f)) | |
| def test_partially_nonexistent(self): | |
| # These annotations start with a non-existent variable and then use | |
| # global types with defined values. This partially evaluates by putting | |
| # those globals into `fwdref.__extra_names__`. | |
| def f( | |
| x: obj | int, | |
| y: container[int:obj, int], | |
| z: dict_val | {str: int}, | |
| alpha: set_val | {str, int}, | |
| beta: obj | bool | int, | |
| gamma: obj | call_func(int, kwd=bool), | |
| ): | |
| pass | |
| def func(*args, **kwargs): | |
| return Union[*args, *(kwargs.values())] | |
| anno = get_annotations(f, format=Format.FORWARDREF) | |
| globals_ = { | |
| "obj": str, "container": list, "dict_val": {1: 2}, "set_val": {1, 2}, | |
| "call_func": func | |
| } | |
| x_anno = anno["x"] | |
| self.assertIsInstance(x_anno, ForwardRef) | |
| self.assertEqual(x_anno.evaluate(globals=globals_), str | int) | |
| y_anno = anno["y"] | |
| self.assertIsInstance(y_anno, ForwardRef) | |
| self.assertEqual(y_anno.evaluate(globals=globals_), list[int:str, int]) | |
| z_anno = anno["z"] | |
| self.assertIsInstance(z_anno, ForwardRef) | |
| self.assertEqual(z_anno.evaluate(globals=globals_), {1: 2} | {str: int}) | |
| alpha_anno = anno["alpha"] | |
| self.assertIsInstance(alpha_anno, ForwardRef) | |
| self.assertEqual(alpha_anno.evaluate(globals=globals_), {1, 2} | {str, int}) | |
| beta_anno = anno["beta"] | |
| self.assertIsInstance(beta_anno, ForwardRef) | |
| self.assertEqual(beta_anno.evaluate(globals=globals_), str | bool | int) | |
| gamma_anno = anno["gamma"] | |
| self.assertIsInstance(gamma_anno, ForwardRef) | |
| self.assertEqual(gamma_anno.evaluate(globals=globals_), str | func(int, kwd=bool)) | |
| def test_partially_nonexistent_union(self): | |
| # Test unions with '|' syntax equal unions with typing.Union[] with some forwardrefs | |
| class UnionForwardrefs: | |
| pipe: str | undefined | |
| union: Union[str, undefined] | |
| annos = get_annotations(UnionForwardrefs, format=Format.FORWARDREF) | |
| pipe = annos["pipe"] | |
| self.assertIsInstance(pipe, ForwardRef) | |
| self.assertEqual( | |
| pipe.evaluate(globals={"undefined": int}), | |
| str | int, | |
| ) | |
| union = annos["union"] | |
| self.assertIsInstance(union, Union) | |
| arg1, arg2 = typing.get_args(union) | |
| self.assertIs(arg1, str) | |
| self.assertEqual( | |
| arg2, support.EqualToForwardRef("undefined", is_class=True, owner=UnionForwardrefs) | |
| ) | |
| class TestStringFormat(unittest.TestCase): | |
| def test_closure(self): | |
| x = 0 | |
| def inner(arg: x): | |
| pass | |
| anno = get_annotations(inner, format=Format.STRING) | |
| self.assertEqual(anno, {"arg": "x"}) | |
| def test_closure_undefined(self): | |
| if False: | |
| x = 0 | |
| def inner(arg: x): | |
| pass | |
| anno = get_annotations(inner, format=Format.STRING) | |
| self.assertEqual(anno, {"arg": "x"}) | |
| def test_function(self): | |
| def f(x: int, y: doesntexist): | |
| pass | |
| anno = get_annotations(f, format=Format.STRING) | |
| self.assertEqual(anno, {"x": "int", "y": "doesntexist"}) | |
| def test_expressions(self): | |
| def f( | |
| add: a + b, | |
| sub: a - b, | |
| mul: a * b, | |
| matmul: a @ b, | |
| truediv: a / b, | |
| mod: a % b, | |
| lshift: a << b, | |
| rshift: a >> b, | |
| or_: a | b, | |
| xor: a ^ b, | |
| and_: a & b, | |
| floordiv: a // b, | |
| pow_: a**b, | |
| lt: a < b, | |
| le: a <= b, | |
| eq: a == b, | |
| ne: a != b, | |
| gt: a > b, | |
| ge: a >= b, | |
| invert: ~a, | |
| neg: -a, | |
| pos: +a, | |
| getitem: a[b], | |
| getattr: a.b, | |
| call: a(b, *c, d=e), # **kwargs are not supported | |
| *args: *a, | |
| ): | |
| pass | |
| anno = get_annotations(f, format=Format.STRING) | |
| self.assertEqual( | |
| anno, | |
| { | |
| "add": "a + b", | |
| "sub": "a - b", | |
| "mul": "a * b", | |
| "matmul": "a @ b", | |
| "truediv": "a / b", | |
| "mod": "a % b", | |
| "lshift": "a << b", | |
| "rshift": "a >> b", | |
| "or_": "a | b", | |
| "xor": "a ^ b", | |
| "and_": "a & b", | |
| "floordiv": "a // b", | |
| "pow_": "a ** b", | |
| "lt": "a < b", | |
| "le": "a <= b", | |
| "eq": "a == b", | |
| "ne": "a != b", | |
| "gt": "a > b", | |
| "ge": "a >= b", | |
| "invert": "~a", | |
| "neg": "-a", | |
| "pos": "+a", | |
| "getitem": "a[b]", | |
| "getattr": "a.b", | |
| "call": "a(b, *c, d=e)", | |
| "args": "*a", | |
| }, | |
| ) | |
| def test_reverse_ops(self): | |
| def f( | |
| radd: 1 + a, | |
| rsub: 1 - a, | |
| rmul: 1 * a, | |
| rmatmul: 1 @ a, | |
| rtruediv: 1 / a, | |
| rmod: 1 % a, | |
| rlshift: 1 << a, | |
| rrshift: 1 >> a, | |
| ror: 1 | a, | |
| rxor: 1 ^ a, | |
| rand: 1 & a, | |
| rfloordiv: 1 // a, | |
| rpow: 1**a, | |
| ): | |
| pass | |
| anno = get_annotations(f, format=Format.STRING) | |
| self.assertEqual( | |
| anno, | |
| { | |
| "radd": "1 + a", | |
| "rsub": "1 - a", | |
| "rmul": "1 * a", | |
| "rmatmul": "1 @ a", | |
| "rtruediv": "1 / a", | |
| "rmod": "1 % a", | |
| "rlshift": "1 << a", | |
| "rrshift": "1 >> a", | |
| "ror": "1 | a", | |
| "rxor": "1 ^ a", | |
| "rand": "1 & a", | |
| "rfloordiv": "1 // a", | |
| "rpow": "1 ** a", | |
| }, | |
| ) | |
| def test_template_str(self): | |
| def f( | |
| x: t"{a}", | |
| y: list[t"{a}"], | |
| z: t"{a:b} {c!r} {d!s:t}", | |
| a: t"a{b}c{d}e{f}g", | |
| b: t"{a:{1}}", | |
| c: t"{a | b * c}", | |
| gh138558: t"{ 0}", | |
| ): pass | |
| annos = get_annotations(f, format=Format.STRING) | |
| self.assertEqual(annos, { | |
| "x": "t'{a}'", | |
| "y": "list[t'{a}']", | |
| "z": "t'{a:b} {c!r} {d!s:t}'", | |
| "a": "t'a{b}c{d}e{f}g'", | |
| # interpolations in the format spec are eagerly evaluated so we can't recover the source | |
| "b": "t'{a:1}'", | |
| "c": "t'{a | b * c}'", | |
| "gh138558": "t'{ 0}'", | |
| }) | |
| def g( | |
| x: t"{a}", | |
| ): ... | |
| annos = get_annotations(g, format=Format.FORWARDREF) | |
| templ = annos["x"] | |
| # Template and Interpolation don't have __eq__ so we have to compare manually | |
| self.assertIsInstance(templ, Template) | |
| self.assertEqual(templ.strings, ("", "")) | |
| self.assertEqual(len(templ.interpolations), 1) | |
| interp = templ.interpolations[0] | |
| self.assertEqual(interp.value, support.EqualToForwardRef("a", owner=g)) | |
| self.assertEqual(interp.expression, "a") | |
| self.assertIsNone(interp.conversion) | |
| self.assertEqual(interp.format_spec, "") | |
| def test_getitem(self): | |
| def f(x: undef1[str, undef2]): | |
| pass | |
| anno = get_annotations(f, format=Format.STRING) | |
| self.assertEqual(anno, {"x": "undef1[str, undef2]"}) | |
| anno = get_annotations(f, format=Format.FORWARDREF) | |
| fwdref = anno["x"] | |
| self.assertIsInstance(fwdref, ForwardRef) | |
| self.assertEqual( | |
| fwdref.evaluate(globals={"undef1": dict, "undef2": float}), dict[str, float] | |
| ) | |
| def test_slice(self): | |
| def f(x: a[b:c]): | |
| pass | |
| anno = get_annotations(f, format=Format.STRING) | |
| self.assertEqual(anno, {"x": "a[b:c]"}) | |
| def f(x: a[b:c, d:e]): | |
| pass | |
| anno = get_annotations(f, format=Format.STRING) | |
| self.assertEqual(anno, {"x": "a[b:c, d:e]"}) | |
| obj = slice(1, 1, 1) | |
| def f(x: obj): | |
| pass | |
| anno = get_annotations(f, format=Format.STRING) | |
| self.assertEqual(anno, {"x": "obj"}) | |
| def test_literals(self): | |
| def f( | |
| a: 1, | |
| b: 1.0, | |
| c: "hello", | |
| d: b"hello", | |
| e: True, | |
| f: None, | |
| g: ..., | |
| h: 1j, | |
| ): | |
| pass | |
| anno = get_annotations(f, format=Format.STRING) | |
| self.assertEqual( | |
| anno, | |
| { | |
| "a": "1", | |
| "b": "1.0", | |
| "c": 'hello', | |
| "d": "b'hello'", | |
| "e": "True", | |
| "f": "None", | |
| "g": "...", | |
| "h": "1j", | |
| }, | |
| ) | |
| def test_displays(self): | |
| # Simple case first | |
| def f(x: a[[int, str], float]): | |
| pass | |
| anno = get_annotations(f, format=Format.STRING) | |
| self.assertEqual(anno, {"x": "a[[int, str], float]"}) | |
| def g( | |
| w: a[[int, str], float], | |
| x: a[{int}, 3], | |
| y: a[{int: str}, 4], | |
| z: a[(int, str), 5], | |
| ): | |
| pass | |
| anno = get_annotations(g, format=Format.STRING) | |
| self.assertEqual( | |
| anno, | |
| { | |
| "w": "a[[int, str], float]", | |
| "x": "a[{int}, 3]", | |
| "y": "a[{int: str}, 4]", | |
| "z": "a[(int, str), 5]", | |
| }, | |
| ) | |
| def test_nested_expressions(self): | |
| def f( | |
| nested: list[Annotated[set[int], "set of ints", 4j]], | |
| set: {a + b}, # single element because order is not guaranteed | |
| dict: {a + b: c + d, "key": e + g}, | |
| list: [a, b, c], | |
| tuple: (a, b, c), | |
| slice: (a[b:c], a[b:c:d], a[:c], a[b:], a[:], a[::d], a[b::d]), | |
| extended_slice: a[:, :, c:d], | |
| unpack1: [*a], | |
| unpack2: [*a, b, c], | |
| ): | |
| pass | |
| anno = get_annotations(f, format=Format.STRING) | |
| self.assertEqual( | |
| anno, | |
| { | |
| "nested": "list[Annotated[set[int], 'set of ints', 4j]]", | |
| "set": "{a + b}", | |
| "dict": "{a + b: c + d, 'key': e + g}", | |
| "list": "[a, b, c]", | |
| "tuple": "(a, b, c)", | |
| "slice": "(a[b:c], a[b:c:d], a[:c], a[b:], a[:], a[::d], a[b::d])", | |
| "extended_slice": "a[:, :, c:d]", | |
| "unpack1": "[*a]", | |
| "unpack2": "[*a, b, c]", | |
| }, | |
| ) | |
| def test_unsupported_operations(self): | |
| format_msg = "Cannot stringify annotation containing string formatting" | |
| def f(fstring: f"{a}"): | |
| pass | |
| with self.assertRaisesRegex(TypeError, format_msg): | |
| get_annotations(f, format=Format.STRING) | |
| def f(fstring_format: f"{a:02d}"): | |
| pass | |
| with self.assertRaisesRegex(TypeError, format_msg): | |
| get_annotations(f, format=Format.STRING) | |
| def test_shenanigans(self): | |
| # In cases like this we can't reconstruct the source; test that we do something | |
| # halfway reasonable. | |
| def f(x: x | (1).__class__, y: (1).__class__): | |
| pass | |
| self.assertEqual( | |
| get_annotations(f, format=Format.STRING), | |
| {"x": "x | <class 'int'>", "y": "<class 'int'>"}, | |
| ) | |
| class TestGetAnnotations(unittest.TestCase): | |
| def test_builtin_type(self): | |
| self.assertEqual(get_annotations(int), {}) | |
| self.assertEqual(get_annotations(object), {}) | |
| def test_custom_metaclass(self): | |
| class Meta(type): | |
| pass | |
| class C(metaclass=Meta): | |
| x: int | |
| self.assertEqual(get_annotations(C), {"x": int}) | |
| def test_missing_dunder_dict(self): | |
| class NoDict(type): | |
| @property | |
| def __dict__(cls): | |
| raise AttributeError | |
| b: str | |
| class C1(metaclass=NoDict): | |
| a: int | |
| self.assertEqual(get_annotations(C1), {"a": int}) | |
| self.assertEqual( | |
| get_annotations(C1, format=Format.FORWARDREF), | |
| {"a": int}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(C1, format=Format.STRING), | |
| {"a": "int"}, | |
| ) | |
| self.assertEqual(get_annotations(NoDict), {"b": str}) | |
| self.assertEqual( | |
| get_annotations(NoDict, format=Format.FORWARDREF), | |
| {"b": str}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(NoDict, format=Format.STRING), | |
| {"b": "str"}, | |
| ) | |
| def test_format(self): | |
| def f1(a: int): | |
| pass | |
| def f2(a: undefined): | |
| pass | |
| self.assertEqual( | |
| get_annotations(f1, format=Format.VALUE), | |
| {"a": int}, | |
| ) | |
| self.assertEqual(get_annotations(f1, format=1), {"a": int}) | |
| fwd = support.EqualToForwardRef("undefined", owner=f2) | |
| self.assertEqual( | |
| get_annotations(f2, format=Format.FORWARDREF), | |
| {"a": fwd}, | |
| ) | |
| self.assertEqual(get_annotations(f2, format=3), {"a": fwd}) | |
| self.assertEqual( | |
| get_annotations(f1, format=Format.STRING), | |
| {"a": "int"}, | |
| ) | |
| self.assertEqual(get_annotations(f1, format=4), {"a": "int"}) | |
| with self.assertRaises(ValueError): | |
| get_annotations(f1, format=42) | |
| with self.assertRaisesRegex( | |
| ValueError, | |
| r"The VALUE_WITH_FAKE_GLOBALS format is for internal use only", | |
| ): | |
| get_annotations(f1, format=Format.VALUE_WITH_FAKE_GLOBALS) | |
| with self.assertRaisesRegex( | |
| ValueError, | |
| r"The VALUE_WITH_FAKE_GLOBALS format is for internal use only", | |
| ): | |
| get_annotations(f1, format=2) | |
| def test_custom_object_with_annotations(self): | |
| class C: | |
| def __init__(self): | |
| self.__annotations__ = {"x": int, "y": str} | |
| self.assertEqual(get_annotations(C()), {"x": int, "y": str}) | |
| def test_custom_format_eval_str(self): | |
| def foo(): | |
| pass | |
| with self.assertRaises(ValueError): | |
| get_annotations(foo, format=Format.FORWARDREF, eval_str=True) | |
| get_annotations(foo, format=Format.STRING, eval_str=True) | |
| def test_eval_str_wrapped_cycle_self(self): | |
| # gh-146556: self-referential __wrapped__ cycle must not hang. | |
| def f(x: 'int') -> 'str': ... | |
| f.__wrapped__ = f | |
| # Cycle is detected and broken; globals from f itself are used. | |
| result = get_annotations(f, eval_str=True) | |
| self.assertEqual(result, {'x': int, 'return': str}) | |
| def test_eval_str_wrapped_partial_cycle_self(self): | |
| def f(x: 'int') -> 'str': ... | |
| f.__wrapped__ = functools.partial(f, 0) | |
| # Cycle is detected and broken; globals from f itself are used. | |
| result = get_annotations(f, eval_str=True) | |
| self.assertEqual(result, {'x': int, 'return': str}) | |
| def test_eval_str_wrapped_cycle_mutual(self): | |
| # gh-146556: mutual __wrapped__ cycle (a -> b -> a) must not hang. | |
| def a(x: 'int'): ... | |
| def b(): ... | |
| a.__wrapped__ = b | |
| b.__wrapped__ = a | |
| result = get_annotations(a, eval_str=True) | |
| self.assertEqual(result, {'x': int}) | |
| def test_eval_str_wrapped_chain_no_cycle(self): | |
| # gh-146556: a valid (non-cyclic) __wrapped__ chain must still work. | |
| def inner(x: 'int'): ... | |
| def outer(x: 'int'): ... | |
| outer.__wrapped__ = inner | |
| result = get_annotations(outer, eval_str=True) | |
| self.assertEqual(result, {'x': int}) | |
| def test_stock_annotations(self): | |
| def foo(a: int, b: str): | |
| pass | |
| for format in (Format.VALUE, Format.FORWARDREF): | |
| with self.subTest(format=format): | |
| self.assertEqual( | |
| get_annotations(foo, format=format), | |
| {"a": int, "b": str}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(foo, format=Format.STRING), | |
| {"a": "int", "b": "str"}, | |
| ) | |
| foo.__annotations__ = {"a": "foo", "b": "str"} | |
| for format in Format: | |
| if format == Format.VALUE_WITH_FAKE_GLOBALS: | |
| continue | |
| with self.subTest(format=format): | |
| self.assertEqual( | |
| get_annotations(foo, format=format), | |
| {"a": "foo", "b": "str"}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(foo, eval_str=True, locals=locals()), | |
| {"a": foo, "b": str}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(foo, eval_str=True, globals=locals()), | |
| {"a": foo, "b": str}, | |
| ) | |
| def test_stock_annotations_in_module(self): | |
| isa = inspect_stock_annotations | |
| for kwargs in [ | |
| {}, | |
| {"eval_str": False}, | |
| {"format": Format.VALUE}, | |
| {"format": Format.FORWARDREF}, | |
| {"format": Format.VALUE, "eval_str": False}, | |
| {"format": Format.FORWARDREF, "eval_str": False}, | |
| ]: | |
| with self.subTest(**kwargs): | |
| self.assertEqual(get_annotations(isa, **kwargs), {"a": int, "b": str}) | |
| self.assertEqual( | |
| get_annotations(isa.MyClass, **kwargs), | |
| {"a": int, "b": str}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.function, **kwargs), | |
| {"a": int, "b": str, "return": isa.MyClass}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.function2, **kwargs), | |
| {"a": int, "b": "str", "c": isa.MyClass, "return": isa.MyClass}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.function3, **kwargs), | |
| {"a": "int", "b": "str", "c": "MyClass"}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(annotationlib, **kwargs), {} | |
| ) # annotations module has no annotations | |
| self.assertEqual(get_annotations(isa.UnannotatedClass, **kwargs), {}) | |
| self.assertEqual( | |
| get_annotations(isa.unannotated_function, **kwargs), | |
| {}, | |
| ) | |
| for kwargs in [ | |
| {"eval_str": True}, | |
| {"format": Format.VALUE, "eval_str": True}, | |
| ]: | |
| with self.subTest(**kwargs): | |
| self.assertEqual(get_annotations(isa, **kwargs), {"a": int, "b": str}) | |
| self.assertEqual( | |
| get_annotations(isa.MyClass, **kwargs), | |
| {"a": int, "b": str}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.function, **kwargs), | |
| {"a": int, "b": str, "return": isa.MyClass}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.function2, **kwargs), | |
| {"a": int, "b": str, "c": isa.MyClass, "return": isa.MyClass}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.function3, **kwargs), | |
| {"a": int, "b": str, "c": isa.MyClass}, | |
| ) | |
| self.assertEqual(get_annotations(annotationlib, **kwargs), {}) | |
| self.assertEqual(get_annotations(isa.UnannotatedClass, **kwargs), {}) | |
| self.assertEqual( | |
| get_annotations(isa.unannotated_function, **kwargs), | |
| {}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa, format=Format.STRING), | |
| {"a": "int", "b": "str"}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.MyClass, format=Format.STRING), | |
| {"a": "int", "b": "str"}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.function, format=Format.STRING), | |
| {"a": "int", "b": "str", "return": "MyClass"}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.function2, format=Format.STRING), | |
| {"a": "int", "b": "str", "c": "MyClass", "return": "MyClass"}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.function3, format=Format.STRING), | |
| {"a": "int", "b": "str", "c": "MyClass"}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(annotationlib, format=Format.STRING), | |
| {}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.UnannotatedClass, format=Format.STRING), | |
| {}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.unannotated_function, format=Format.STRING), | |
| {}, | |
| ) | |
| def test_stock_annotations_on_wrapper(self): | |
| isa = inspect_stock_annotations | |
| wrapped = times_three(isa.function) | |
| self.assertEqual(wrapped(1, "x"), isa.MyClass(3, "xxx")) | |
| self.assertIsNot(wrapped.__globals__, isa.function.__globals__) | |
| self.assertEqual( | |
| get_annotations(wrapped), | |
| {"a": int, "b": str, "return": isa.MyClass}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(wrapped, format=Format.FORWARDREF), | |
| {"a": int, "b": str, "return": isa.MyClass}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(wrapped, format=Format.STRING), | |
| {"a": "int", "b": "str", "return": "MyClass"}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(wrapped, eval_str=True), | |
| {"a": int, "b": str, "return": isa.MyClass}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(wrapped, eval_str=False), | |
| {"a": int, "b": str, "return": isa.MyClass}, | |
| ) | |
| def test_stringized_annotations_in_module(self): | |
| isa = inspect_stringized_annotations | |
| for kwargs in [ | |
| {}, | |
| {"eval_str": False}, | |
| {"format": Format.VALUE}, | |
| {"format": Format.FORWARDREF}, | |
| {"format": Format.STRING}, | |
| {"format": Format.VALUE, "eval_str": False}, | |
| {"format": Format.FORWARDREF, "eval_str": False}, | |
| {"format": Format.STRING, "eval_str": False}, | |
| ]: | |
| with self.subTest(**kwargs): | |
| self.assertEqual( | |
| get_annotations(isa, **kwargs), | |
| {"a": "int", "b": "str"}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.MyClass, **kwargs), | |
| {"a": "int", "b": "str"}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.function, **kwargs), | |
| {"a": "int", "b": "str", "return": "MyClass"}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.function2, **kwargs), | |
| {"a": "int", "b": "'str'", "c": "MyClass", "return": "MyClass"}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.function3, **kwargs), | |
| {"a": "'int'", "b": "'str'", "c": "'MyClass'"}, | |
| ) | |
| self.assertEqual(get_annotations(isa.UnannotatedClass, **kwargs), {}) | |
| self.assertEqual( | |
| get_annotations(isa.unannotated_function, **kwargs), | |
| {}, | |
| ) | |
| for kwargs in [ | |
| {"eval_str": True}, | |
| {"eval_str": True, "globals": isa.__dict__, "locals": {}}, | |
| {"eval_str": True, "globals": {}, "locals": isa.__dict__}, | |
| {"format": Format.VALUE, "eval_str": True}, | |
| ]: | |
| with self.subTest(**kwargs): | |
| self.assertEqual(get_annotations(isa, **kwargs), {"a": int, "b": str}) | |
| self.assertEqual( | |
| get_annotations(isa.MyClass, **kwargs), | |
| {"a": int, "b": str}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.function, **kwargs), | |
| {"a": int, "b": str, "return": isa.MyClass}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.function2, **kwargs), | |
| {"a": int, "b": "str", "c": isa.MyClass, "return": isa.MyClass}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.function3, **kwargs), | |
| {"a": "int", "b": "str", "c": "MyClass"}, | |
| ) | |
| self.assertEqual(get_annotations(isa.UnannotatedClass, **kwargs), {}) | |
| self.assertEqual( | |
| get_annotations(isa.unannotated_function, **kwargs), | |
| {}, | |
| ) | |
| def test_stringized_annotations_in_empty_module(self): | |
| isa2 = inspect_stringized_annotations_2 | |
| self.assertEqual(get_annotations(isa2), {}) | |
| self.assertEqual(get_annotations(isa2, eval_str=True), {}) | |
| self.assertEqual(get_annotations(isa2, eval_str=False), {}) | |
| def test_stringized_annotations_with_star_unpack(self): | |
| def f(*args: "*tuple[int, ...]"): ... | |
| self.assertEqual(get_annotations(f, eval_str=True), | |
| {'args': (*tuple[int, ...],)[0]}) | |
| def f(*args: " *tuple[int, ...]"): ... | |
| self.assertEqual(get_annotations(f, eval_str=True), | |
| {'args': (*tuple[int, ...],)[0]}) | |
| def test_stringized_annotations_on_wrapper(self): | |
| isa = inspect_stringized_annotations | |
| wrapped = times_three(isa.function) | |
| self.assertEqual(wrapped(1, "x"), isa.MyClass(3, "xxx")) | |
| self.assertIsNot(wrapped.__globals__, isa.function.__globals__) | |
| self.assertEqual( | |
| get_annotations(wrapped), | |
| {"a": "int", "b": "str", "return": "MyClass"}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(wrapped, eval_str=True), | |
| {"a": int, "b": str, "return": isa.MyClass}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(wrapped, eval_str=False), | |
| {"a": "int", "b": "str", "return": "MyClass"}, | |
| ) | |
| def test_stringized_annotations_on_partial_wrapper(self): | |
| isa = inspect_stringized_annotations | |
| def times_three_str(fn: typing.Callable[[str], isa.MyClass]): | |
| @functools.wraps(fn) | |
| def wrapper(b: "str") -> "MyClass": | |
| return fn(b * 3) | |
| return wrapper | |
| wrapped = times_three_str(functools.partial(isa.function, 1)) | |
| self.assertEqual(wrapped("x"), isa.MyClass(1, "xxx")) | |
| self.assertIsNot(wrapped.__globals__, isa.function.__globals__) | |
| self.assertEqual( | |
| get_annotations(wrapped, eval_str=True), | |
| {"b": str, "return": isa.MyClass}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(wrapped, eval_str=False), | |
| {"b": "str", "return": "MyClass"}, | |
| ) | |
| # If functools is not loaded, names will be evaluated in the current | |
| # module instead of being unwrapped to the original. | |
| functools_mod = sys.modules["functools"] | |
| del sys.modules["functools"] | |
| self.assertEqual( | |
| get_annotations(wrapped, eval_str=True), | |
| {"b": str, "return": MyClass}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(wrapped, eval_str=False), | |
| {"b": "str", "return": "MyClass"}, | |
| ) | |
| sys.modules["functools"] = functools_mod | |
| def test_stringized_annotations_on_class(self): | |
| isa = inspect_stringized_annotations | |
| # test that local namespace lookups work | |
| self.assertEqual( | |
| get_annotations(isa.MyClassWithLocalAnnotations), | |
| {"x": "mytype"}, | |
| ) | |
| self.assertEqual( | |
| get_annotations(isa.MyClassWithLocalAnnotations, eval_str=True), | |
| {"x": int}, | |
| ) | |
| def test_stringized_annotations_on_custom_object(self): | |
| class HasAnnotations: | |
| @property | |
| def __annotations__(self): | |
| return {"x": "int"} | |
| ha = HasAnnotations() | |
| self.assertEqual(get_annotations(ha), {"x": "int"}) | |
| self.assertEqual(get_annotations(ha, eval_str=True), {"x": int}) | |
| def test_stringized_annotation_permutations(self): | |
| def define_class(name, has_future, has_annos, base_text, extra_names=None): | |
| lines = [] | |
| if has_future: | |
| lines.append("from __future__ import annotations") | |
| lines.append(f"class {name}({base_text}):") | |
| if has_annos: | |
| lines.append(f" {name}_attr: int") | |
| else: | |
| lines.append(" pass") | |
| code = "\n".join(lines) | |
| ns = support.run_code(code, extra_names=extra_names) | |
| return ns[name] | |
| def check_annotations(cls, has_future, has_annos): | |
| if has_annos: | |
| if has_future: | |
| anno = "int" | |
| else: | |
| anno = int | |
| self.assertEqual(get_annotations(cls), {f"{cls.__name__}_attr": anno}) | |
| else: | |
| self.assertEqual(get_annotations(cls), {}) | |
| for meta_future, base_future, child_future, meta_has_annos, base_has_annos, child_has_annos in itertools.product( | |
| (False, True), | |
| (False, True), | |
| (False, True), | |
| (False, True), | |
| (False, True), | |
| (False, True), | |
| ): | |
| with self.subTest( | |
| meta_future=meta_future, | |
| base_future=base_future, | |
| child_future=child_future, | |
| meta_has_annos=meta_has_annos, | |
| base_has_annos=base_has_annos, | |
| child_has_annos=child_has_annos, | |
| ): | |
| meta = define_class( | |
| "Meta", | |
| has_future=meta_future, | |
| has_annos=meta_has_annos, | |
| base_text="type", | |
| ) | |
| base = define_class( | |
| "Base", | |
| has_future=base_future, | |
| has_annos=base_has_annos, | |
| base_text="metaclass=Meta", | |
| extra_names={"Meta": meta}, | |
| ) | |
| child = define_class( | |
| "Child", | |
| has_future=child_future, | |
| has_annos=child_has_annos, | |
| base_text="Base", | |
| extra_names={"Base": base}, | |
| ) | |
| check_annotations(meta, meta_future, meta_has_annos) | |
| check_annotations(base, base_future, base_has_annos) | |
| check_annotations(child, child_future, child_has_annos) | |
| def test_modify_annotations(self): | |
| def f(x: int): | |
| pass | |
| self.assertEqual(get_annotations(f), {"x": int}) | |
| self.assertEqual( | |
| get_annotations(f, format=Format.FORWARDREF), | |
| {"x": int}, | |
| ) | |
| f.__annotations__["x"] = str | |
| # The modification is reflected in VALUE (the default) | |
| self.assertEqual(get_annotations(f), {"x": str}) | |
| # ... and also in FORWARDREF, which tries __annotations__ if available | |
| self.assertEqual( | |
| get_annotations(f, format=Format.FORWARDREF), | |
| {"x": str}, | |
| ) | |
| # ... but not in STRING which always uses __annotate__ | |
| self.assertEqual( | |
| get_annotations(f, format=Format.STRING), | |
| {"x": "int"}, | |
| ) | |
| def test_non_dict_annotations(self): | |
| class WeirdAnnotations: | |
| @property | |
| def __annotations__(self): | |
| return "not a dict" | |
| wa = WeirdAnnotations() | |
| for format in Format: | |
| if format == Format.VALUE_WITH_FAKE_GLOBALS: | |
| continue | |
| with ( | |
| self.subTest(format=format), | |
| self.assertRaisesRegex( | |
| ValueError, r".*__annotations__ is neither a dict nor None" | |
| ), | |
| ): | |
| get_annotations(wa, format=format) | |
| def test_annotations_on_custom_object(self): | |
| class HasAnnotations: | |
| @property | |
| def __annotations__(self): | |
| return {"x": int} | |
| ha = HasAnnotations() | |
| self.assertEqual(get_annotations(ha, format=Format.VALUE), {"x": int}) | |
| self.assertEqual(get_annotations(ha, format=Format.FORWARDREF), {"x": int}) | |
| self.assertEqual(get_annotations(ha, format=Format.STRING), {"x": "int"}) | |
| def test_raising_annotations_on_custom_object(self): | |
| class HasRaisingAnnotations: | |
| @property | |
| def __annotations__(self): | |
| return {"x": undefined} | |
| hra = HasRaisingAnnotations() | |
| with self.assertRaises(NameError): | |
| get_annotations(hra, format=Format.VALUE) | |
| with self.assertRaises(NameError): | |
| get_annotations(hra, format=Format.FORWARDREF) | |
| undefined = float | |
| self.assertEqual(get_annotations(hra, format=Format.VALUE), {"x": float}) | |
| def test_forwardref_prefers_annotations(self): | |
| class HasBoth: | |
| @property | |
| def __annotations__(self): | |
| return {"x": int} | |
| @property | |
| def __annotate__(self): | |
| return lambda format: {"x": str} | |
| hb = HasBoth() | |
| self.assertEqual(get_annotations(hb, format=Format.VALUE), {"x": int}) | |
| self.assertEqual(get_annotations(hb, format=Format.FORWARDREF), {"x": int}) | |
| self.assertEqual(get_annotations(hb, format=Format.STRING), {"x": str}) | |
| def test_only_annotate(self): | |
| def f(x: int): | |
| pass | |
| class OnlyAnnotate: | |
| @property | |
| def __annotate__(self): | |
| return f.__annotate__ | |
| oa = OnlyAnnotate() | |
| self.assertEqual(get_annotations(oa, format=Format.VALUE), {"x": int}) | |
| self.assertEqual(get_annotations(oa, format=Format.FORWARDREF), {"x": int}) | |
| self.assertEqual( | |
| get_annotations(oa, format=Format.STRING), | |
| {"x": "int"}, | |
| ) | |
| def test_non_dict_annotate(self): | |
| class WeirdAnnotate: | |
| def __annotate__(self, *args, **kwargs): | |
| return "not a dict" | |
| wa = WeirdAnnotate() | |
| for format in Format: | |
| if format == Format.VALUE_WITH_FAKE_GLOBALS: | |
| continue | |
| with ( | |
| self.subTest(format=format), | |
| self.assertRaisesRegex( | |
| ValueError, r".*__annotate__ returned a non-dict" | |
| ), | |
| ): | |
| get_annotations(wa, format=format) | |
| def test_no_annotations(self): | |
| class CustomClass: | |
| pass | |
| class MyCallable: | |
| def __call__(self): | |
| pass | |
| for format in Format: | |
| if format == Format.VALUE_WITH_FAKE_GLOBALS: | |
| continue | |
| for obj in (None, 1, object(), CustomClass()): | |
| with self.subTest(format=format, obj=obj): | |
| with self.assertRaises(TypeError): | |
| get_annotations(obj, format=format) | |
| # Callables and types with no annotations return an empty dict | |
| for obj in (int, len, MyCallable()): | |
| with self.subTest(format=format, obj=obj): | |
| self.assertEqual(get_annotations(obj, format=format), {}) | |
| def test_pep695_generic_class_with_future_annotations(self): | |
| ann_module695 = inspect_stringized_annotations_pep695 | |
| A_annotations = get_annotations(ann_module695.A, eval_str=True) | |
| A_type_params = ann_module695.A.__type_params__ | |
| self.assertIs(A_annotations["x"], A_type_params[0]) | |
| self.assertEqual(A_annotations["y"].__args__[0], Unpack[A_type_params[1]]) | |
| self.assertIs(A_annotations["z"].__args__[0], A_type_params[2]) | |
| def test_pep695_generic_class_with_future_annotations_and_local_shadowing(self): | |
| B_annotations = get_annotations( | |
| inspect_stringized_annotations_pep695.B, eval_str=True | |
| ) | |
| self.assertEqual(B_annotations, {"x": int, "y": str, "z": bytes}) | |
| def test_pep695_generic_class_with_future_annotations_name_clash_with_global_vars( | |
| self, | |
| ): | |
| ann_module695 = inspect_stringized_annotations_pep695 | |
| C_annotations = get_annotations(ann_module695.C, eval_str=True) | |
| self.assertEqual( | |
| set(C_annotations.values()), set(ann_module695.C.__type_params__) | |
| ) | |
| def test_pep_695_generic_function_with_future_annotations(self): | |
| ann_module695 = inspect_stringized_annotations_pep695 | |
| generic_func_annotations = get_annotations( | |
| ann_module695.generic_function, eval_str=True | |
| ) | |
| func_t_params = ann_module695.generic_function.__type_params__ | |
| self.assertEqual( | |
| generic_func_annotations.keys(), {"x", "y", "z", "zz", "return"} | |
| ) | |
| self.assertIs(generic_func_annotations["x"], func_t_params[0]) | |
| self.assertEqual(generic_func_annotations["y"], Unpack[func_t_params[1]]) | |
| self.assertIs(generic_func_annotations["z"].__origin__, func_t_params[2]) | |
| self.assertIs(generic_func_annotations["zz"].__origin__, func_t_params[2]) | |
| def test_pep_695_generic_function_with_future_annotations_name_clash_with_global_vars( | |
| self, | |
| ): | |
| self.assertEqual( | |
| set( | |
| get_annotations( | |
| inspect_stringized_annotations_pep695.generic_function_2, | |
| eval_str=True, | |
| ).values() | |
| ), | |
| set( | |
| inspect_stringized_annotations_pep695.generic_function_2.__type_params__ | |
| ), | |
| ) | |
| def test_pep_695_generic_method_with_future_annotations(self): | |
| ann_module695 = inspect_stringized_annotations_pep695 | |
| generic_method_annotations = get_annotations( | |
| ann_module695.D.generic_method, eval_str=True | |
| ) | |
| params = { | |
| param.__name__: param | |
| for param in ann_module695.D.generic_method.__type_params__ | |
| } | |
| self.assertEqual( | |
| generic_method_annotations, | |
| {"x": params["Foo"], "y": params["Bar"], "return": None}, | |
| ) | |
| def test_pep_695_generic_method_with_future_annotations_name_clash_with_global_vars( | |
| self, | |
| ): | |
| self.assertEqual( | |
| set( | |
| get_annotations( | |
| inspect_stringized_annotations_pep695.D.generic_method_2, | |
| eval_str=True, | |
| ).values() | |
| ), | |
| set( | |
| inspect_stringized_annotations_pep695.D.generic_method_2.__type_params__ | |
| ), | |
| ) | |
| def test_pep_695_generic_method_with_future_annotations_name_clash_with_global_and_local_vars( | |
| self, | |
| ): | |
| self.assertEqual( | |
| get_annotations(inspect_stringized_annotations_pep695.E, eval_str=True), | |
| {"x": str}, | |
| ) | |
| def test_pep_695_generics_with_future_annotations_nested_in_function(self): | |
| results = inspect_stringized_annotations_pep695.nested() | |
| self.assertEqual( | |
| set(results.F_annotations.values()), set(results.F.__type_params__) | |
| ) | |
| self.assertEqual( | |
| set(results.F_meth_annotations.values()), | |
| set(results.F.generic_method.__type_params__), | |
| ) | |
| self.assertNotEqual( | |
| set(results.F_meth_annotations.values()), set(results.F.__type_params__) | |
| ) | |
| self.assertEqual( | |
| set(results.F_meth_annotations.values()).intersection( | |
| results.F.__type_params__ | |
| ), | |
| set(), | |
| ) | |
| self.assertEqual(results.G_annotations, {"x": str}) | |
| self.assertEqual( | |
| set(results.generic_func_annotations.values()), | |
| set(results.generic_func.__type_params__), | |
| ) | |
| def test_partial_evaluation(self): | |
| def f( | |
| x: builtins.undef, | |
| y: list[int], | |
| z: 1 + int, | |
| a: builtins.int, | |
| b: [builtins.undef, builtins.int], | |
| ): | |
| pass | |
| self.assertEqual( | |
| get_annotations(f, format=Format.FORWARDREF), | |
| { | |
| "x": support.EqualToForwardRef("builtins.undef", owner=f), | |
| "y": list[int], | |
| "z": support.EqualToForwardRef("1 + int", owner=f), | |
| "a": int, | |
| "b": [ | |
| support.EqualToForwardRef("builtins.undef", owner=f), | |
| # We can't resolve this because we have to evaluate the whole annotation | |
| support.EqualToForwardRef("builtins.int", owner=f), | |
| ], | |
| }, | |
| ) | |
| self.assertEqual( | |
| get_annotations(f, format=Format.STRING), | |
| { | |
| "x": "builtins.undef", | |
| "y": "list[int]", | |
| "z": "1 + int", | |
| "a": "builtins.int", | |
| "b": "[builtins.undef, builtins.int]", | |
| }, | |
| ) | |
| def test_partial_evaluation_error(self): | |
| def f(x: range[1]): | |
| pass | |
| with self.assertRaisesRegex( | |
| TypeError, "type 'range' is not subscriptable" | |
| ): | |
| f.__annotations__ | |
| self.assertEqual( | |
| get_annotations(f, format=Format.FORWARDREF), | |
| { | |
| "x": support.EqualToForwardRef("range[1]", owner=f), | |
| }, | |
| ) | |
| def test_partial_evaluation_cell(self): | |
| obj = object() | |
| class RaisesAttributeError: | |
| attriberr: obj.missing | |
| anno = get_annotations(RaisesAttributeError, format=Format.FORWARDREF) | |
| self.assertEqual( | |
| anno, | |
| { | |
| "attriberr": support.EqualToForwardRef( | |
| "obj.missing", is_class=True, owner=RaisesAttributeError | |
| ) | |
| }, | |
| ) | |
| def test_nonlocal_in_annotation_scope(self): | |
| class Demo: | |
| nonlocal sequence_b | |
| x: sequence_b | |
| y: sequence_b[int] | |
| fwdrefs = get_annotations(Demo, format=Format.FORWARDREF) | |
| self.assertIsInstance(fwdrefs["x"], ForwardRef) | |
| self.assertIsInstance(fwdrefs["y"], ForwardRef) | |
| sequence_b = list | |
| self.assertIs(fwdrefs["x"].evaluate(), list) | |
| self.assertEqual(fwdrefs["y"].evaluate(), list[int]) | |
| def test_raises_error_from_value(self): | |
| # test that if VALUE is the only supported format, but raises an error | |
| # that error is propagated from get_annotations | |
| class DemoException(Exception): ... | |
| def annotate(format, /): | |
| if format == Format.VALUE: | |
| raise DemoException() | |
| else: | |
| raise NotImplementedError(format) | |
| def f(): ... | |
| f.__annotate__ = annotate | |
| for fmt in [Format.VALUE, Format.FORWARDREF, Format.STRING]: | |
| with self.assertRaises(DemoException): | |
| get_annotations(f, format=fmt) | |
| class TestCallEvaluateFunction(unittest.TestCase): | |
| def test_evaluation(self): | |
| def evaluate(format, exc=NotImplementedError): | |
| if format > 2: | |
| raise exc | |
| return undefined | |
| with self.assertRaises(NameError): | |
| annotationlib.call_evaluate_function(evaluate, Format.VALUE) | |
| self.assertEqual( | |
| annotationlib.call_evaluate_function(evaluate, Format.FORWARDREF), | |
| support.EqualToForwardRef("undefined"), | |
| ) | |
| self.assertEqual( | |
| annotationlib.call_evaluate_function(evaluate, Format.STRING), | |
| "undefined", | |
| ) | |
| def test_fake_global_evaluation(self): | |
| # This will raise an AttributeError | |
| def evaluate_union(format, exc=NotImplementedError): | |
| if format == Format.VALUE_WITH_FAKE_GLOBALS: | |
| # Return a ForwardRef | |
| return builtins.undefined | list[int] | |
| raise exc | |
| self.assertEqual( | |
| annotationlib.call_evaluate_function(evaluate_union, Format.FORWARDREF), | |
| support.EqualToForwardRef("builtins.undefined | list[int]"), | |
| ) | |
| # This will raise an AttributeError | |
| def evaluate_intermediate(format, exc=NotImplementedError): | |
| if format == Format.VALUE_WITH_FAKE_GLOBALS: | |
| intermediate = builtins.undefined | |
| # Return a literal | |
| return intermediate is None | |
| raise exc | |
| self.assertIs( | |
| annotationlib.call_evaluate_function(evaluate_intermediate, Format.FORWARDREF), | |
| False, | |
| ) | |
| class TestCallAnnotateFunction(unittest.TestCase): | |
| # Tests for user defined annotate functions. | |
| # Format and NotImplementedError are provided as arguments so they exist in | |
| # the fake globals namespace. | |
| # This avoids non-matching conditions passing by being converted to stringifiers. | |
| # See: https://github.com/python/cpython/issues/138764 | |
| def test_user_annotate_value(self): | |
| def annotate(format, /): | |
| if format == Format.VALUE: | |
| return {"x": str} | |
| else: | |
| raise NotImplementedError(format) | |
| annotations = annotationlib.call_annotate_function( | |
| annotate, | |
| Format.VALUE, | |
| ) | |
| self.assertEqual(annotations, {"x": str}) | |
| def test_user_annotate_forwardref_supported(self): | |
| # If Format.FORWARDREF is supported prefer it over Format.VALUE | |
| def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError): | |
| if format == __Format.VALUE: | |
| return {'x': str} | |
| elif format == __Format.VALUE_WITH_FAKE_GLOBALS: | |
| return {'x': int} | |
| elif format == __Format.FORWARDREF: | |
| return {'x': float} | |
| else: | |
| raise __NotImplementedError(format) | |
| annotations = annotationlib.call_annotate_function( | |
| annotate, | |
| Format.FORWARDREF | |
| ) | |
| self.assertEqual(annotations, {"x": float}) | |
| def test_user_annotate_forwardref_fakeglobals(self): | |
| # If Format.FORWARDREF is not supported, use Format.VALUE_WITH_FAKE_GLOBALS | |
| # before falling back to Format.VALUE | |
| def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError): | |
| if format == __Format.VALUE: | |
| return {'x': str} | |
| elif format == __Format.VALUE_WITH_FAKE_GLOBALS: | |
| return {'x': int} | |
| else: | |
| raise __NotImplementedError(format) | |
| annotations = annotationlib.call_annotate_function( | |
| annotate, | |
| Format.FORWARDREF | |
| ) | |
| self.assertEqual(annotations, {"x": int}) | |
| def test_user_annotate_forwardref_value_fallback(self): | |
| # If Format.FORWARDREF and Format.VALUE_WITH_FAKE_GLOBALS are not supported | |
| # use Format.VALUE | |
| def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError): | |
| if format == __Format.VALUE: | |
| return {"x": str} | |
| else: | |
| raise __NotImplementedError(format) | |
| annotations = annotationlib.call_annotate_function( | |
| annotate, | |
| Format.FORWARDREF, | |
| ) | |
| self.assertEqual(annotations, {"x": str}) | |
| def test_user_annotate_string_supported(self): | |
| # If Format.STRING is supported prefer it over Format.VALUE | |
| def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError): | |
| if format == __Format.VALUE: | |
| return {'x': str} | |
| elif format == __Format.VALUE_WITH_FAKE_GLOBALS: | |
| return {'x': int} | |
| elif format == __Format.STRING: | |
| return {'x': "float"} | |
| else: | |
| raise __NotImplementedError(format) | |
| annotations = annotationlib.call_annotate_function( | |
| annotate, | |
| Format.STRING, | |
| ) | |
| self.assertEqual(annotations, {"x": "float"}) | |
| def test_user_annotate_string_fakeglobals(self): | |
| # If Format.STRING is not supported but Format.VALUE_WITH_FAKE_GLOBALS is | |
| # prefer that over Format.VALUE | |
| def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError): | |
| if format == __Format.VALUE: | |
| return {'x': str} | |
| elif format == __Format.VALUE_WITH_FAKE_GLOBALS: | |
| return {'x': int} | |
| else: | |
| raise __NotImplementedError(format) | |
| annotations = annotationlib.call_annotate_function( | |
| annotate, | |
| Format.STRING, | |
| ) | |
| self.assertEqual(annotations, {"x": "int"}) | |
| def test_user_annotate_string_value_fallback(self): | |
| # If Format.STRING and Format.VALUE_WITH_FAKE_GLOBALS are not | |
| # supported fall back to Format.VALUE and convert to strings | |
| def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError): | |
| if format == __Format.VALUE: | |
| return {"x": str} | |
| else: | |
| raise __NotImplementedError(format) | |
| annotations = annotationlib.call_annotate_function( | |
| annotate, | |
| Format.STRING, | |
| ) | |
| self.assertEqual(annotations, {"x": "str"}) | |
| def test_condition_not_stringified(self): | |
| # Make sure the first condition isn't evaluated as True by being converted | |
| # to a _Stringifier | |
| def annotate(format, /): | |
| if format == Format.FORWARDREF: | |
| return {"x": str} | |
| else: | |
| raise NotImplementedError(format) | |
| with self.assertRaises(NotImplementedError): | |
| annotationlib.call_annotate_function(annotate, Format.STRING) | |
| def test_unsupported_formats(self): | |
| def annotate(format, /): | |
| if format == Format.FORWARDREF: | |
| return {"x": str} | |
| else: | |
| raise NotImplementedError(format) | |
| with self.assertRaises(ValueError): | |
| annotationlib.call_annotate_function(annotate, Format.VALUE_WITH_FAKE_GLOBALS) | |
| with self.assertRaises(RuntimeError): | |
| annotationlib.call_annotate_function(annotate, Format.VALUE) | |
| with self.assertRaises(ValueError): | |
| # Some non-Format value | |
| annotationlib.call_annotate_function(annotate, 7) | |
| def test_basic_non_function_annotate(self): | |
| class Annotate: | |
| def __call__(self, format, /, __Format=Format, | |
| __NotImplementedError=NotImplementedError): | |
| if format == __Format.VALUE: | |
| return {'x': str} | |
| elif format == __Format.VALUE_WITH_FAKE_GLOBALS: | |
| return {'x': int} | |
| elif format == __Format.STRING: | |
| return {'x': "float"} | |
| else: | |
| raise __NotImplementedError(format) | |
| annotations = annotationlib.call_annotate_function(Annotate(), Format.VALUE) | |
| self.assertEqual(annotations, {"x": str}) | |
| annotations = annotationlib.call_annotate_function(Annotate(), Format.STRING) | |
| self.assertEqual(annotations, {"x": "float"}) | |
| with self.assertRaises(AttributeError) as cm: | |
| annotations = annotationlib.call_annotate_function( | |
| Annotate(), Format.FORWARDREF | |
| ) | |
| self.assertEqual(cm.exception.name, "__builtins__") | |
| self.assertIsInstance(cm.exception.obj, Annotate) | |
| def test_full_non_function_annotate(self): | |
| def outer(): | |
| local = str | |
| class Annotate: | |
| called_formats = [] | |
| def __call__(self, format=None, *, _self=None): | |
| nonlocal local | |
| if _self is not None: | |
| self, format = _self, self | |
| self.called_formats.append(format) | |
| if format == 1: # VALUE | |
| return {"x": MyClass, "y": int, "z": local} | |
| if format == 2: # VALUE_WITH_FAKE_GLOBALS | |
| return {"w": unknown, "x": MyClass, "y": int, "z": local} | |
| raise NotImplementedError | |
| __globals__ = {"MyClass": MyClass} | |
| __builtins__ = {"int": int} | |
| __closure__ = (types.CellType(str),) | |
| __defaults__ = (None,) | |
| __kwdefaults__ = property(lambda self: dict(_self=self)) | |
| __code__ = property(lambda self: self.__call__.__code__) | |
| return Annotate() | |
| annotate = outer() | |
| self.assertEqual( | |
| annotationlib.call_annotate_function(annotate, Format.VALUE), | |
| {"x": MyClass, "y": int, "z": str} | |
| ) | |
| self.assertEqual(annotate.called_formats[-1], Format.VALUE) | |
| self.assertEqual( | |
| annotationlib.call_annotate_function(annotate, Format.STRING), | |
| {"w": "unknown", "x": "MyClass", "y": "int", "z": "local"} | |
| ) | |
| self.assertIn(Format.STRING, annotate.called_formats) | |
| self.assertEqual(annotate.called_formats[-1], Format.VALUE_WITH_FAKE_GLOBALS) | |
| self.assertEqual( | |
| annotationlib.call_annotate_function(annotate, Format.FORWARDREF), | |
| {"w": support.EqualToForwardRef("unknown"), "x": MyClass, "y": int, "z": str} | |
| ) | |
| self.assertIn(Format.FORWARDREF, annotate.called_formats) | |
| self.assertEqual(annotate.called_formats[-1], Format.VALUE_WITH_FAKE_GLOBALS) | |
| def test_error_from_value_raised(self): | |
| # Test that the error from format.VALUE is raised | |
| # if all formats fail | |
| class DemoException(Exception): ... | |
| def annotate(format, /): | |
| if format == Format.VALUE: | |
| raise DemoException() | |
| else: | |
| raise NotImplementedError(format) | |
| for fmt in [Format.VALUE, Format.FORWARDREF, Format.STRING]: | |
| with self.assertRaises(DemoException): | |
| annotationlib.call_annotate_function(annotate, format=fmt) | |
| def test_generated_annotate_non_bool_compare_result(self): | |
| class NonBool(int): | |
| def __gt__(self, other): | |
| return None | |
| # Function __annotate__ | |
| def func(x: int): | |
| pass | |
| self.assertEqual( | |
| func.__annotate__(NonBool(1)), | |
| {"x": int}, | |
| ) | |
| # Class __annotate__ | |
| class C: | |
| y: int | |
| self.assertEqual( | |
| C.__annotate__(NonBool(1)), | |
| {"y": int}, | |
| ) | |
| # Module __annotate__ | |
| ns = {} | |
| exec("z: int", ns) | |
| self.assertEqual( | |
| ns["__annotate__"](NonBool(1)), | |
| {"z": int}, | |
| ) | |
| class MetaclassTests(unittest.TestCase): | |
| def test_annotated_meta(self): | |
| class Meta(type): | |
| a: int | |
| class X(metaclass=Meta): | |
| pass | |
| class Y(metaclass=Meta): | |
| b: float | |
| self.assertEqual(get_annotations(Meta), {"a": int}) | |
| self.assertEqual(Meta.__annotate__(Format.VALUE), {"a": int}) | |
| self.assertEqual(get_annotations(X), {}) | |
| self.assertIs(X.__annotate__, None) | |
| self.assertEqual(get_annotations(Y), {"b": float}) | |
| self.assertEqual(Y.__annotate__(Format.VALUE), {"b": float}) | |
| def test_unannotated_meta(self): | |
| class Meta(type): | |
| pass | |
| class X(metaclass=Meta): | |
| a: str | |
| class Y(X): | |
| pass | |
| self.assertEqual(get_annotations(Meta), {}) | |
| self.assertIs(Meta.__annotate__, None) | |
| self.assertEqual(get_annotations(Y), {}) | |
| self.assertIs(Y.__annotate__, None) | |
| self.assertEqual(get_annotations(X), {"a": str}) | |
| self.assertEqual(X.__annotate__(Format.VALUE), {"a": str}) | |
| def test_ordering(self): | |
| # Based on a sample by David Ellis | |
| # https://discuss.python.org/t/pep-749-implementing-pep-649/54974/38 | |
| def make_classes(): | |
| class Meta(type): | |
| a: int | |
| expected_annotations = {"a": int} | |
| class A(type, metaclass=Meta): | |
| b: float | |
| expected_annotations = {"b": float} | |
| class B(metaclass=A): | |
| c: str | |
| expected_annotations = {"c": str} | |
| class C(B): | |
| expected_annotations = {} | |
| class D(metaclass=Meta): | |
| expected_annotations = {} | |
| return Meta, A, B, C, D | |
| classes = make_classes() | |
| class_count = len(classes) | |
| for order in itertools.permutations(range(class_count), class_count): | |
| names = ", ".join(classes[i].__name__ for i in order) | |
| with self.subTest(names=names): | |
| classes = make_classes() # Regenerate classes | |
| for i in order: | |
| get_annotations(classes[i]) | |
| for c in classes: | |
| with self.subTest(c=c): | |
| self.assertEqual(get_annotations(c), c.expected_annotations) | |
| annotate_func = getattr(c, "__annotate__", None) | |
| if c.expected_annotations: | |
| self.assertEqual( | |
| annotate_func(Format.VALUE), c.expected_annotations | |
| ) | |
| else: | |
| self.assertIs(annotate_func, None) | |
| class TestGetAnnotateFromClassNamespace(unittest.TestCase): | |
| def test_with_metaclass(self): | |
| class Meta(type): | |
| def __new__(mcls, name, bases, ns): | |
| annotate = annotationlib.get_annotate_from_class_namespace(ns) | |
| expected = ns["expected_annotate"] | |
| with self.subTest(name=name): | |
| if expected: | |
| self.assertIsNotNone(annotate) | |
| else: | |
| self.assertIsNone(annotate) | |
| return super().__new__(mcls, name, bases, ns) | |
| class HasAnnotations(metaclass=Meta): | |
| expected_annotate = True | |
| a: int | |
| class NoAnnotations(metaclass=Meta): | |
| expected_annotate = False | |
| class CustomAnnotate(metaclass=Meta): | |
| expected_annotate = True | |
| def __annotate__(format): | |
| return {} | |
| code = """ | |
| from __future__ import annotations | |
| class HasFutureAnnotations(metaclass=Meta): | |
| expected_annotate = False | |
| a: int | |
| """ | |
| exec(textwrap.dedent(code), {"Meta": Meta}) | |
| class TestTypeRepr(unittest.TestCase): | |
| def test_type_repr(self): | |
| class Nested: | |
| pass | |
| def nested(): | |
| pass | |
| self.assertEqual(type_repr(int), "int") | |
| self.assertEqual(type_repr(MyClass), f"{__name__}.MyClass") | |
| self.assertEqual( | |
| type_repr(Nested), f"{__name__}.TestTypeRepr.test_type_repr.<locals>.Nested" | |
| ) | |
| self.assertEqual( | |
| type_repr(nested), f"{__name__}.TestTypeRepr.test_type_repr.<locals>.nested" | |
| ) | |
| self.assertEqual(type_repr(len), "len") | |
| # gh-152692: built-in methods have __module__ set to None | |
| self.assertEqual(type_repr([].append), "list.append") | |
| self.assertEqual(type_repr((1).bit_length), "int.bit_length") | |
| self.assertEqual(type_repr(dict.fromkeys), "dict.fromkeys") | |
| # Methods of extension types also have __module__ set to None, but | |
| # are not in the builtins module. | |
| import datetime | |
| self.assertEqual( | |
| type_repr(datetime.datetime(2000, 1, 1).isoformat), | |
| "datetime.datetime.isoformat", | |
| ) | |
| self.assertEqual(type_repr(datetime.date.today), "datetime.date.today") | |
| self.assertEqual(type_repr(type_repr), "annotationlib.type_repr") | |
| self.assertEqual(type_repr(times_three), f"{__name__}.times_three") | |
| self.assertEqual(type_repr(...), "...") | |
| self.assertEqual(type_repr(None), "None") | |
| self.assertEqual(type_repr(1), "1") | |
| self.assertEqual(type_repr("1"), "'1'") | |
| self.assertEqual(type_repr(Format.VALUE), repr(Format.VALUE)) | |
| self.assertEqual(type_repr(MyClass()), "my repr") | |
| # gh138558 tests | |
| self.assertEqual(type_repr(t'''{ 0 | |
| & 1 | |
| | 2 | |
| }'''), 't"""{ 0\n & 1\n | 2\n }"""') | |
| self.assertEqual( | |
| type_repr(Template("hi", Interpolation(42, "42"))), "t'hi{42}'" | |
| ) | |
| self.assertEqual( | |
| type_repr(Template("hi", Interpolation(42))), | |
| "Template('hi', Interpolation(42, '', None, ''))", | |
| ) | |
| self.assertEqual( | |
| type_repr(Template("hi", Interpolation(42, " "))), | |
| "Template('hi', Interpolation(42, ' ', None, ''))", | |
| ) | |
| self.assertEqual( | |
| type_repr(Template("hi", Interpolation(42, "4!2"))), | |
| "Template('hi', Interpolation(42, '4!2', None, ''))", | |
| ) | |
| # gh138558: perhaps in the future, we can improve this behavior: | |
| self.assertEqual(type_repr(Template(Interpolation(42, "99"))), "t'{99}'") | |
| class TestAnnotationsToString(unittest.TestCase): | |
| def test_annotations_to_string(self): | |
| self.assertEqual(annotations_to_string({}), {}) | |
| self.assertEqual(annotations_to_string({"x": int}), {"x": "int"}) | |
| self.assertEqual(annotations_to_string({"x": "int"}), {"x": "int"}) | |
| self.assertEqual( | |
| annotations_to_string({"x": int, "y": str}), {"x": "int", "y": "str"} | |
| ) | |
| class A: | |
| pass | |
| TypeParamsAlias1 = int | |
| class TypeParamsSample[TypeParamsAlias1, TypeParamsAlias2]: | |
| TypeParamsAlias2 = str | |
| class TestForwardRefClass(unittest.TestCase): | |
| def test_forwardref_instance_type_error(self): | |
| fr = ForwardRef("int") | |
| with self.assertRaises(TypeError): | |
| isinstance(42, fr) | |
| def test_forwardref_subclass_type_error(self): | |
| fr = ForwardRef("int") | |
| with self.assertRaises(TypeError): | |
| issubclass(int, fr) | |
| def test_forwardref_only_str_arg(self): | |
| with self.assertRaises(TypeError): | |
| ForwardRef(1) # only `str` type is allowed | |
| def test_forward_equality(self): | |
| fr = ForwardRef("int") | |
| self.assertEqual(fr, ForwardRef("int")) | |
| self.assertNotEqual(List["int"], List[int]) | |
| self.assertNotEqual(fr, ForwardRef("int", module=__name__)) | |
| frm = ForwardRef("int", module=__name__) | |
| self.assertEqual(frm, ForwardRef("int", module=__name__)) | |
| self.assertNotEqual(frm, ForwardRef("int", module="__other_name__")) | |
| def test_forward_equality_get_type_hints(self): | |
| c1 = ForwardRef("C") | |
| c1_gth = ForwardRef("C") | |
| c2 = ForwardRef("C") | |
| c2_gth = ForwardRef("C") | |
| class C: | |
| pass | |
| def foo(a: c1_gth, b: c2_gth): | |
| pass | |
| self.assertEqual(get_type_hints(foo, globals(), locals()), {"a": C, "b": C}) | |
| self.assertEqual(c1, c2) | |
| self.assertEqual(c1, c1_gth) | |
| self.assertEqual(c1_gth, c2_gth) | |
| self.assertEqual(List[c1], List[c1_gth]) | |
| self.assertNotEqual(List[c1], List[C]) | |
| self.assertNotEqual(List[c1_gth], List[C]) | |
| self.assertEqual(Union[c1, c1_gth], Union[c1]) | |
| self.assertEqual(Union[c1, c1_gth, int], Union[c1, int]) | |
| def test_forward_equality_hash(self): | |
| c1 = ForwardRef("int") | |
| c1_gth = ForwardRef("int") | |
| c2 = ForwardRef("int") | |
| c2_gth = ForwardRef("int") | |
| def foo(a: c1_gth, b: c2_gth): | |
| pass | |
| get_type_hints(foo, globals(), locals()) | |
| self.assertEqual(hash(c1), hash(c2)) | |
| self.assertEqual(hash(c1_gth), hash(c2_gth)) | |
| self.assertEqual(hash(c1), hash(c1_gth)) | |
| c3 = ForwardRef("int", module=__name__) | |
| c4 = ForwardRef("int", module="__other_name__") | |
| self.assertNotEqual(hash(c3), hash(c1)) | |
| self.assertNotEqual(hash(c3), hash(c1_gth)) | |
| self.assertNotEqual(hash(c3), hash(c4)) | |
| self.assertEqual(hash(c3), hash(ForwardRef("int", module=__name__))) | |
| def test_forward_equality_and_hash_with_cells(self): | |
| """Regression test for GH-143831.""" | |
| class A: | |
| def one(_) -> C1: | |
| """One cell.""" | |
| one_f = ForwardRef("C1", owner=one) | |
| one_f_ga1 = get_annotations(one, format=Format.FORWARDREF)["return"] | |
| one_f_ga2 = get_annotations(one, format=Format.FORWARDREF)["return"] | |
| self.assertIsInstance(one_f_ga1.__cell__, types.CellType) | |
| self.assertIs(one_f_ga1.__cell__, one_f_ga2.__cell__) | |
| def two(_) -> C1 | C2: | |
| """Two cells.""" | |
| two_f_ga1 = get_annotations(two, format=Format.FORWARDREF)["return"] | |
| two_f_ga2 = get_annotations(two, format=Format.FORWARDREF)["return"] | |
| self.assertIsNot(two_f_ga1.__cell__, two_f_ga2.__cell__) | |
| self.assertIsInstance(two_f_ga1.__cell__, dict) | |
| self.assertIsInstance(two_f_ga2.__cell__, dict) | |
| type C1 = None | |
| type C2 = None | |
| self.assertNotEqual(A.one_f, A.one_f_ga1) | |
| self.assertNotEqual(hash(A.one_f), hash(A.one_f_ga1)) | |
| self.assertEqual(A.one_f_ga1, A.one_f_ga2) | |
| self.assertEqual(hash(A.one_f_ga1), hash(A.one_f_ga2)) | |
| self.assertEqual(A.two_f_ga1, A.two_f_ga2) | |
| self.assertEqual(hash(A.two_f_ga1), hash(A.two_f_ga2)) | |
| def test_forward_equality_namespace(self): | |
| def namespace1(): | |
| a = ForwardRef("A") | |
| def fun(x: a): | |
| pass | |
| get_type_hints(fun, globals(), locals()) | |
| return a | |
| def namespace2(): | |
| a = ForwardRef("A") | |
| class A: | |
| pass | |
| def fun(x: a): | |
| pass | |
| get_type_hints(fun, globals(), locals()) | |
| return a | |
| self.assertEqual(namespace1(), namespace1()) | |
| self.assertEqual(namespace1(), namespace2()) | |
| def test_forward_repr(self): | |
| self.assertEqual(repr(List["int"]), "typing.List[ForwardRef('int')]") | |
| self.assertEqual( | |
| repr(List[ForwardRef("int", module="mod")]), | |
| "typing.List[ForwardRef('int', module='mod')]", | |
| ) | |
| self.assertEqual( | |
| repr(List[ForwardRef("int", module="mod", is_class=True)]), | |
| "typing.List[ForwardRef('int', module='mod', is_class=True)]", | |
| ) | |
| self.assertEqual( | |
| repr(List[ForwardRef("int", owner="class")]), | |
| "typing.List[ForwardRef('int', owner='class')]", | |
| ) | |
| def test_forward_repr_extra_names(self): | |
| def f(a: undefined | str): ... | |
| annos = get_annotations(f, format=Format.FORWARDREF) | |
| self.assertRegex( | |
| repr(annos['a']), r"ForwardRef\('undefined \| str'.*\)" | |
| ) | |
| def test_forward_recursion_actually(self): | |
| def namespace1(): | |
| a = ForwardRef("A") | |
| A = a | |
| def fun(x: a): | |
| pass | |
| ret = get_type_hints(fun, globals(), locals()) | |
| return a | |
| def namespace2(): | |
| a = ForwardRef("A") | |
| A = a | |
| def fun(x: a): | |
| pass | |
| ret = get_type_hints(fun, globals(), locals()) | |
| return a | |
| r1 = namespace1() | |
| r2 = namespace2() | |
| self.assertIsNot(r1, r2) | |
| self.assertEqual(r1, r2) | |
| def test_syntax_error(self): | |
| with self.assertRaises(SyntaxError): | |
| typing.Generic["/T"] | |
| def test_delayed_syntax_error(self): | |
| def foo(a: "Node[T"): | |
| pass | |
| with self.assertRaises(SyntaxError): | |
| get_type_hints(foo) | |
| def test_syntax_error_empty_string(self): | |
| for form in [typing.List, typing.Set, typing.Type, typing.Deque]: | |
| with self.subTest(form=form): | |
| with self.assertRaises(SyntaxError): | |
| form[""] | |
| def test_or(self): | |
| X = ForwardRef("X") | |
| # __or__/__ror__ itself | |
| self.assertEqual(X | "x", Union[X, "x"]) | |
| self.assertEqual("x" | X, Union["x", X]) | |
| def test_multiple_ways_to_create(self): | |
| X1 = Union["X"] | |
| self.assertIsInstance(X1, ForwardRef) | |
| X2 = ForwardRef("X") | |
| self.assertIsInstance(X2, ForwardRef) | |
| self.assertEqual(X1, X2) | |
| def test_special_attrs(self): | |
| # Forward refs provide a different introspection API. __name__ and | |
| # __qualname__ make little sense for forward refs as they can store | |
| # complex typing expressions. | |
| fr = ForwardRef("set[Any]") | |
| self.assertNotHasAttr(fr, "__name__") | |
| self.assertNotHasAttr(fr, "__qualname__") | |
| self.assertEqual(fr.__module__, "annotationlib") | |
| # Forward refs are currently unpicklable once they contain a code object. | |
| fr.__forward_code__ # fill the cache | |
| for proto in range(pickle.HIGHEST_PROTOCOL + 1): | |
| with self.assertRaises(TypeError): | |
| pickle.dumps(fr, proto) | |
| def test_evaluate_string_format(self): | |
| fr = ForwardRef("set[Any]") | |
| self.assertEqual(fr.evaluate(format=Format.STRING), "set[Any]") | |
| def test_evaluate_string_format_extra_names(self): | |
| # Test that internal extra_names are replaced when evaluating as strings | |
| def f(a: unknown | str | int | list[str] | tuple[int, ...]): ... | |
| fr = get_annotations(f, format=Format.FORWARDREF)['a'] | |
| # Test the cache is not populated before access | |
| self.assertIsNone(fr.__resolved_str_cache__) | |
| self.assertEqual(fr.evaluate(format=Format.STRING), "unknown | str | int | list[str] | tuple[int, ...]") | |
| # Test that the cache is now set correctly | |
| self.assertEqual(fr.__resolved_str_cache__, "unknown | str | int | list[str] | tuple[int, ...]") | |
| # Test that future evaluations return the same cache | |
| self.assertIs(fr.evaluate(format=Format.STRING), fr.__resolved_str_cache__) | |
| def test_evaluate_forwardref_format(self): | |
| fr = ForwardRef("undef") | |
| evaluated = fr.evaluate(format=Format.FORWARDREF) | |
| self.assertIs(fr, evaluated) | |
| fr = ForwardRef("set[undefined]") | |
| evaluated = fr.evaluate(format=Format.FORWARDREF) | |
| self.assertEqual( | |
| evaluated, | |
| set[support.EqualToForwardRef("undefined")], | |
| ) | |
| fr = ForwardRef("a + b") | |
| self.assertEqual( | |
| fr.evaluate(format=Format.FORWARDREF), | |
| support.EqualToForwardRef("a + b"), | |
| ) | |
| self.assertEqual( | |
| fr.evaluate(format=Format.FORWARDREF, locals={"a": 1, "b": 2}), | |
| 3, | |
| ) | |
| fr = ForwardRef('"a" + 1') | |
| self.assertEqual( | |
| fr.evaluate(format=Format.FORWARDREF), | |
| support.EqualToForwardRef('"a" + 1'), | |
| ) | |
| def test_evaluate_lazy_import(self): | |
| ns = {} | |
| exec( | |
| textwrap.dedent( | |
| """ | |
| lazy from test.test_lazy_import.data.basic2 import x | |
| lazy from test.test_lazy_import.data.broken_module import y | |
| class A: | |
| a: x | |
| class B: | |
| b: y | |
| """ | |
| ), | |
| ns, | |
| ) | |
| self.addCleanup( | |
| import_helper.unload, "test.test_lazy_import.data.basic2" | |
| ) | |
| self.addCleanup( | |
| import_helper.unload, "test.test_lazy_import.data.broken_module" | |
| ) | |
| self.assertIs(type(ns["x"]), types.LazyImportType) | |
| self.assertIs(type(ns["y"]), types.LazyImportType) | |
| # The lazy import resolves successfully: | |
| for format in (Format.VALUE, Format.FORWARDREF): | |
| with self.subTest(format=format): | |
| self.assertEqual( | |
| ForwardRef("x").evaluate(globals=ns, format=format), 42 | |
| ) | |
| self.assertEqual( | |
| ForwardRef("x").evaluate(locals=ns, format=format), 42 | |
| ) | |
| self.assertEqual( | |
| get_annotations(ns["A"], format=Format.FORWARDREF), {"a": 42} | |
| ) | |
| # The lazy import fails to resolve: | |
| fr = ForwardRef("y") | |
| with self.assertRaisesRegex(ValueError, "always fails to import"): | |
| fr.evaluate(globals=ns, format=Format.VALUE) | |
| with self.assertRaisesRegex(ValueError, "always fails to import"): | |
| fr.evaluate(locals=ns, format=Format.VALUE) | |
| self.assertIs(fr.evaluate(globals=ns, format=Format.FORWARDREF), fr) | |
| self.assertIs(fr.evaluate(locals=ns, format=Format.FORWARDREF), fr) | |
| annos = get_annotations(ns["B"], format=Format.FORWARDREF) | |
| self.assertEqual( | |
| annos, | |
| {"b": support.EqualToForwardRef("y", is_class=True, owner=ns["B"])}, | |
| ) | |
| with self.assertRaisesRegex(ValueError, "always fails to import"): | |
| annos["b"].evaluate(format=Format.VALUE) | |
| with self.assertRaisesRegex(ValueError, "always fails to import"): | |
| get_annotations(ns["B"], format=Format.VALUE) | |
| def test_get_annotations_lazy_import(self): | |
| ns = {} | |
| exec( | |
| textwrap.dedent( | |
| """ | |
| lazy from test.test_lazy_import.data.basic2 import x | |
| lazy from test.test_lazy_import.data.broken_module import y | |
| class A: | |
| a: object.fail | |
| b: x | |
| class B: | |
| a: object.fail | |
| b: y | |
| """ | |
| ), | |
| ns, | |
| ) | |
| self.addCleanup( | |
| import_helper.unload, "test.test_lazy_import.data.basic2" | |
| ) | |
| self.addCleanup( | |
| import_helper.unload, "test.test_lazy_import.data.broken_module" | |
| ) | |
| self.assertIs(type(ns["x"]), types.LazyImportType) | |
| self.assertIs(type(ns["y"]), types.LazyImportType) | |
| annos = get_annotations(ns["A"], format=Format.FORWARDREF) | |
| self.assertEqual( | |
| annos, | |
| { | |
| "a": support.EqualToForwardRef( | |
| "object.fail", is_class=True, owner=ns["A"] | |
| ), | |
| "b": 42, | |
| }, | |
| ) | |
| annos = get_annotations(ns["B"], format=Format.FORWARDREF) | |
| self.assertEqual( | |
| annos, | |
| { | |
| "a": support.EqualToForwardRef( | |
| "object.fail", is_class=True, owner=ns["B"] | |
| ), | |
| "b": support.EqualToForwardRef( | |
| "y", is_class=True, owner=ns["B"] | |
| ), | |
| }, | |
| ) | |
| with self.assertRaisesRegex(ValueError, "always fails to import"): | |
| annos["b"].evaluate(format=Format.VALUE) | |
| def test_evaluate_notimplemented_format(self): | |
| class C: | |
| x: alias | |
| fwdref = get_annotations(C, format=Format.FORWARDREF)["x"] | |
| with self.assertRaises(NotImplementedError): | |
| fwdref.evaluate(format=Format.VALUE_WITH_FAKE_GLOBALS) | |
| with self.assertRaises(NotImplementedError): | |
| # Some other unsupported value | |
| fwdref.evaluate(format=7) | |
| def test_evaluate_with_type_params(self): | |
| class Gen[T]: | |
| alias = int | |
| with self.assertRaises(NameError): | |
| ForwardRef("T").evaluate() | |
| with self.assertRaises(NameError): | |
| ForwardRef("T").evaluate(type_params=()) | |
| with self.assertRaises(NameError): | |
| ForwardRef("T").evaluate(owner=int) | |
| (T,) = Gen.__type_params__ | |
| self.assertIs(ForwardRef("T").evaluate(type_params=Gen.__type_params__), T) | |
| self.assertIs(ForwardRef("T").evaluate(owner=Gen), T) | |
| with self.assertRaises(NameError): | |
| ForwardRef("alias").evaluate(type_params=Gen.__type_params__) | |
| self.assertIs(ForwardRef("alias").evaluate(owner=Gen), int) | |
| # If you pass custom locals, we don't look at the owner's locals | |
| with self.assertRaises(NameError): | |
| ForwardRef("alias").evaluate(owner=Gen, locals={}) | |
| # But if the name exists in the locals, it works | |
| self.assertIs( | |
| ForwardRef("alias").evaluate(owner=Gen, locals={"alias": str}), str | |
| ) | |
| def test_evaluate_with_type_params_and_scope_conflict(self): | |
| for is_class in (False, True): | |
| with self.subTest(is_class=is_class): | |
| fwdref1 = ForwardRef("TypeParamsAlias1", owner=TypeParamsSample, is_class=is_class) | |
| fwdref2 = ForwardRef("TypeParamsAlias2", owner=TypeParamsSample, is_class=is_class) | |
| self.assertIs( | |
| fwdref1.evaluate(), | |
| TypeParamsSample.__type_params__[0], | |
| ) | |
| self.assertIs( | |
| fwdref2.evaluate(), | |
| TypeParamsSample.TypeParamsAlias2, | |
| ) | |
| def test_fwdref_with_module(self): | |
| self.assertIs(ForwardRef("Format", module="annotationlib").evaluate(), Format) | |
| self.assertIs( | |
| ForwardRef("Counter", module="collections").evaluate(), collections.Counter | |
| ) | |
| self.assertEqual( | |
| ForwardRef("Counter[int]", module="collections").evaluate(), | |
| collections.Counter[int], | |
| ) | |
| with self.assertRaises(NameError): | |
| # If globals are passed explicitly, we don't look at the module dict | |
| ForwardRef("Format", module="annotationlib").evaluate(globals={}) | |
| def test_fwdref_to_builtin(self): | |
| self.assertIs(ForwardRef("int").evaluate(), int) | |
| self.assertIs(ForwardRef("int", module="collections").evaluate(), int) | |
| self.assertIs(ForwardRef("int", owner=str).evaluate(), int) | |
| # builtins are still searched with explicit globals | |
| self.assertIs(ForwardRef("int").evaluate(globals={}), int) | |
| # explicit values in globals have precedence | |
| obj = object() | |
| self.assertIs(ForwardRef("int").evaluate(globals={"int": obj}), obj) | |
| def test_fwdref_value_is_not_cached(self): | |
| fr = ForwardRef("hello") | |
| with self.assertRaises(NameError): | |
| fr.evaluate() | |
| self.assertIs(fr.evaluate(globals={"hello": str}), str) | |
| with self.assertRaises(NameError): | |
| fr.evaluate() | |
| def test_fwdref_with_owner(self): | |
| self.assertEqual( | |
| ForwardRef("Counter[int]", owner=collections).evaluate(), | |
| collections.Counter[int], | |
| ) | |
| def test_name_lookup_without_eval(self): | |
| # test the codepath where we look up simple names directly in the | |
| # namespaces without going through eval() | |
| self.assertIs(ForwardRef("int").evaluate(), int) | |
| self.assertIs(ForwardRef("int").evaluate(locals={"int": str}), str) | |
| self.assertIs( | |
| ForwardRef("int").evaluate(locals={"int": float}, globals={"int": str}), | |
| float, | |
| ) | |
| self.assertIs(ForwardRef("int").evaluate(globals={"int": str}), str) | |
| with support.swap_attr(builtins, "int", dict): | |
| self.assertIs(ForwardRef("int").evaluate(), dict) | |
| with self.assertRaises(NameError, msg="name 'doesntexist' is not defined") as exc: | |
| ForwardRef("doesntexist").evaluate() | |
| self.assertEqual(exc.exception.name, "doesntexist") | |
| def test_evaluate_undefined_generic(self): | |
| # Test the codepath where have to eval() with undefined variables. | |
| class C: | |
| x: alias[int, undef] | |
| generic = get_annotations(C, format=Format.FORWARDREF)["x"].evaluate( | |
| format=Format.FORWARDREF, | |
| globals={"alias": dict} | |
| ) | |
| self.assertNotIsInstance(generic, ForwardRef) | |
| self.assertIs(generic.__origin__, dict) | |
| self.assertEqual(len(generic.__args__), 2) | |
| self.assertIs(generic.__args__[0], int) | |
| self.assertIsInstance(generic.__args__[1], ForwardRef) | |
| generic = get_annotations(C, format=Format.FORWARDREF)["x"].evaluate( | |
| format=Format.FORWARDREF, | |
| globals={"alias": Union}, | |
| locals={"alias": dict} | |
| ) | |
| self.assertNotIsInstance(generic, ForwardRef) | |
| self.assertIs(generic.__origin__, dict) | |
| self.assertEqual(len(generic.__args__), 2) | |
| self.assertIs(generic.__args__[0], int) | |
| self.assertIsInstance(generic.__args__[1], ForwardRef) | |
| def test_fwdref_invalid_syntax(self): | |
| fr = ForwardRef("if") | |
| with self.assertRaises(SyntaxError): | |
| fr.evaluate() | |
| fr = ForwardRef("1+") | |
| with self.assertRaises(SyntaxError): | |
| fr.evaluate() | |
| def test_re_evaluate_generics(self): | |
| global global_alias | |
| # If we've already run this test before, | |
| # ensure the variable is still undefined | |
| if "global_alias" in globals(): | |
| del global_alias | |
| class C: | |
| x: global_alias[int] | |
| # Evaluate the ForwardRef once | |
| evaluated = get_annotations(C, format=Format.FORWARDREF)["x"].evaluate( | |
| format=Format.FORWARDREF | |
| ) | |
| # Now define the global and ensure that the ForwardRef evaluates | |
| global_alias = list | |
| self.assertEqual(evaluated.evaluate(), list[int]) | |
| def test_fwdref_evaluate_argument_mutation(self): | |
| class C[T]: | |
| nonlocal alias | |
| x: alias[T] | |
| # Mutable arguments | |
| globals_ = globals() | |
| globals_copy = globals_.copy() | |
| locals_ = locals() | |
| locals_copy = locals_.copy() | |
| # Evaluate the ForwardRef, ensuring we use __cell__ and type params | |
| get_annotations(C, format=Format.FORWARDREF)["x"].evaluate( | |
| globals=globals_, | |
| locals=locals_, | |
| type_params=C.__type_params__, | |
| format=Format.FORWARDREF, | |
| ) | |
| # Check if the passed in mutable arguments equal the originals | |
| self.assertEqual(globals_, globals_copy) | |
| self.assertEqual(locals_, locals_copy) | |
| alias = list | |
| def test_fwdref_final_class(self): | |
| with self.assertRaises(TypeError): | |
| class C(ForwardRef): | |
| pass | |
| class TestAnnotationLib(unittest.TestCase): | |
| def test__all__(self): | |
| support.check__all__(self, annotationlib) | |
| @support.cpython_only | |
| def test_lazy_imports(self): | |
| import_helper.ensure_lazy_imports( | |
| "annotationlib", | |
| { | |
| "typing", | |
| "warnings", | |
| }, | |
| ) |