"""Tests for the annotations module.""" import textwrap import annotationlib import builtins import collections import functools import itertools import pickle from string.templatelib import Template import typing 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_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, ): 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)) 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}", ): 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}'", }) 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 | ", "y": ""}, ) 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_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}, {"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_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_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_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_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 ) }, ) 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", ) 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..Nested" ) self.assertEqual( type_repr(nested), f"{__name__}.TestTypeRepr.test_type_repr..nested" ) self.assertEqual(type_repr(len), "len") 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") 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 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_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')]", ) 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_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_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_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): ForwardRef("doesntexist").evaluate() def test_fwdref_invalid_syntax(self): fr = ForwardRef("if") with self.assertRaises(SyntaxError): fr.evaluate() fr = ForwardRef("1+") with self.assertRaises(SyntaxError): fr.evaluate() 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", }, )