aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/Lib/test/test_annotationlib.py
diff options
context:
space:
mode:
Diffstat (limited to 'Lib/test/test_annotationlib.py')
-rw-r--r--Lib/test/test_annotationlib.py381
1 files changed, 357 insertions, 24 deletions
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index be55f044b15..ae0e73f08c5 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -1,18 +1,19 @@
"""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,
- get_annotate_function,
annotations_to_string,
type_repr,
)
@@ -121,6 +122,28 @@ class TestForwardRefFormat(unittest.TestCase):
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):
@@ -251,6 +274,126 @@ class TestStringFormat(unittest.TestCase):
},
)
+ 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]],
@@ -296,6 +439,17 @@ class TestStringFormat(unittest.TestCase):
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):
@@ -661,6 +815,70 @@ class TestGetAnnotations(unittest.TestCase):
{"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
@@ -901,6 +1119,73 @@ class TestGetAnnotations(unittest.TestCase):
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):
@@ -933,13 +1218,13 @@ class MetaclassTests(unittest.TestCase):
b: float
self.assertEqual(get_annotations(Meta), {"a": int})
- self.assertEqual(get_annotate_function(Meta)(Format.VALUE), {"a": int})
+ self.assertEqual(Meta.__annotate__(Format.VALUE), {"a": int})
self.assertEqual(get_annotations(X), {})
- self.assertIs(get_annotate_function(X), None)
+ self.assertIs(X.__annotate__, None)
self.assertEqual(get_annotations(Y), {"b": float})
- self.assertEqual(get_annotate_function(Y)(Format.VALUE), {"b": float})
+ self.assertEqual(Y.__annotate__(Format.VALUE), {"b": float})
def test_unannotated_meta(self):
class Meta(type):
@@ -952,13 +1237,13 @@ class MetaclassTests(unittest.TestCase):
pass
self.assertEqual(get_annotations(Meta), {})
- self.assertIs(get_annotate_function(Meta), None)
+ self.assertIs(Meta.__annotate__, None)
self.assertEqual(get_annotations(Y), {})
- self.assertIs(get_annotate_function(Y), None)
+ self.assertIs(Y.__annotate__, None)
self.assertEqual(get_annotations(X), {"a": str})
- self.assertEqual(get_annotate_function(X)(Format.VALUE), {"a": str})
+ self.assertEqual(X.__annotate__(Format.VALUE), {"a": str})
def test_ordering(self):
# Based on a sample by David Ellis
@@ -996,7 +1281,7 @@ class MetaclassTests(unittest.TestCase):
for c in classes:
with self.subTest(c=c):
self.assertEqual(get_annotations(c), c.expected_annotations)
- annotate_func = get_annotate_function(c)
+ annotate_func = getattr(c, "__annotate__", None)
if c.expected_annotations:
self.assertEqual(
annotate_func(Format.VALUE), c.expected_annotations
@@ -1005,25 +1290,39 @@ class MetaclassTests(unittest.TestCase):
self.assertIs(annotate_func, None)
-class TestGetAnnotateFunction(unittest.TestCase):
- def test_static_class(self):
- self.assertIsNone(get_annotate_function(object))
- self.assertIsNone(get_annotate_function(int))
-
- def test_unannotated_class(self):
- class C:
- pass
+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
- self.assertIsNone(get_annotate_function(C))
+ class NoAnnotations(metaclass=Meta):
+ expected_annotate = False
- D = type("D", (), {})
- self.assertIsNone(get_annotate_function(D))
+ class CustomAnnotate(metaclass=Meta):
+ expected_annotate = True
+ def __annotate__(format):
+ return {}
- def test_annotated_class(self):
- class C:
- a: int
+ code = """
+ from __future__ import annotations
- self.assertEqual(get_annotate_function(C)(Format.VALUE), {"a": int})
+ class HasFutureAnnotations(metaclass=Meta):
+ expected_annotate = False
+ a: int
+ """
+ exec(textwrap.dedent(code), {"Meta": Meta})
class TestTypeRepr(unittest.TestCase):
@@ -1240,6 +1539,38 @@ class TestForwardRefClass(unittest.TestCase):
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
@@ -1319,9 +1650,11 @@ class TestForwardRefClass(unittest.TestCase):
with support.swap_attr(builtins, "int", dict):
self.assertIs(ForwardRef("int").evaluate(), dict)
- with self.assertRaises(NameError):
+ with self.assertRaises(NameError, msg="name 'doesntexist' is not defined") as exc:
ForwardRef("doesntexist").evaluate()
+ self.assertEqual(exc.exception.name, "doesntexist")
+
def test_fwdref_invalid_syntax(self):
fr = ForwardRef("if")
with self.assertRaises(SyntaxError):