mirror of https://github.com/python/cpython
1094 lines
38 KiB
Python
1094 lines
38 KiB
Python
"""Tests for the annotations module."""
|
|
|
|
import annotationlib
|
|
import builtins
|
|
import collections
|
|
import functools
|
|
import itertools
|
|
import pickle
|
|
import unittest
|
|
from annotationlib import (
|
|
Format,
|
|
ForwardRef,
|
|
get_annotations,
|
|
get_annotate_function,
|
|
annotations_to_string,
|
|
value_to_string,
|
|
)
|
|
from typing import Unpack
|
|
|
|
from test import support
|
|
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.FORWARDREF.value, 2)
|
|
self.assertEqual(Format.FORWARDREF, 2)
|
|
|
|
self.assertEqual(Format.STRING.value, 3)
|
|
self.assertEqual(Format.STRING, 3)
|
|
|
|
|
|
class TestForwardRefFormat(unittest.TestCase):
|
|
def test_closure(self):
|
|
def inner(arg: x):
|
|
pass
|
|
|
|
anno = annotationlib.get_annotations(inner, format=Format.FORWARDREF)
|
|
fwdref = anno["arg"]
|
|
self.assertIsInstance(fwdref, annotationlib.ForwardRef)
|
|
self.assertEqual(fwdref.__forward_arg__, "x")
|
|
with self.assertRaises(NameError):
|
|
fwdref.evaluate()
|
|
|
|
x = 1
|
|
self.assertEqual(fwdref.evaluate(), x)
|
|
|
|
anno = annotationlib.get_annotations(inner, format=Format.FORWARDREF)
|
|
self.assertEqual(anno["arg"], x)
|
|
|
|
def test_function(self):
|
|
def f(x: int, y: doesntexist):
|
|
pass
|
|
|
|
anno = annotationlib.get_annotations(f, format=Format.FORWARDREF)
|
|
self.assertIs(anno["x"], int)
|
|
fwdref = anno["y"]
|
|
self.assertIsInstance(fwdref, annotationlib.ForwardRef)
|
|
self.assertEqual(fwdref.__forward_arg__, "doesntexist")
|
|
with self.assertRaises(NameError):
|
|
fwdref.evaluate()
|
|
self.assertEqual(fwdref.evaluate(globals={"doesntexist": 1}), 1)
|
|
|
|
|
|
class TestSourceFormat(unittest.TestCase):
|
|
def test_closure(self):
|
|
x = 0
|
|
|
|
def inner(arg: x):
|
|
pass
|
|
|
|
anno = annotationlib.get_annotations(inner, format=Format.STRING)
|
|
self.assertEqual(anno, {"arg": "x"})
|
|
|
|
def test_function(self):
|
|
def f(x: int, y: doesntexist):
|
|
pass
|
|
|
|
anno = annotationlib.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 = annotationlib.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 = annotationlib.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_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 = annotationlib.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):
|
|
annotationlib.get_annotations(f, format=Format.STRING)
|
|
|
|
def f(fstring_format: f"{a:02d}"):
|
|
pass
|
|
|
|
with self.assertRaisesRegex(TypeError, format_msg):
|
|
annotationlib.get_annotations(f, format=Format.STRING)
|
|
|
|
|
|
class TestForwardRefClass(unittest.TestCase):
|
|
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 = annotationlib.ForwardRef("set[Any]")
|
|
self.assertFalse(hasattr(fr, "__name__"))
|
|
self.assertFalse(hasattr(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_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_cached(self):
|
|
fr = ForwardRef("hello")
|
|
with self.assertRaises(NameError):
|
|
fr.evaluate()
|
|
self.assertIs(fr.evaluate(globals={"hello": str}), str)
|
|
self.assertIs(fr.evaluate(), str)
|
|
|
|
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 TestGetAnnotations(unittest.TestCase):
|
|
def test_builtin_type(self):
|
|
self.assertEqual(annotationlib.get_annotations(int), {})
|
|
self.assertEqual(annotationlib.get_annotations(object), {})
|
|
|
|
def test_custom_metaclass(self):
|
|
class Meta(type):
|
|
pass
|
|
|
|
class C(metaclass=Meta):
|
|
x: int
|
|
|
|
self.assertEqual(annotationlib.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(annotationlib.get_annotations(C1), {"a": int})
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(C1, format=Format.FORWARDREF),
|
|
{"a": int},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(C1, format=Format.STRING),
|
|
{"a": "int"},
|
|
)
|
|
self.assertEqual(annotationlib.get_annotations(NoDict), {"b": str})
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(NoDict, format=Format.FORWARDREF),
|
|
{"b": str},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(NoDict, format=Format.STRING),
|
|
{"b": "str"},
|
|
)
|
|
|
|
def test_format(self):
|
|
def f1(a: int):
|
|
pass
|
|
|
|
def f2(a: undefined):
|
|
pass
|
|
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(f1, format=Format.VALUE),
|
|
{"a": int},
|
|
)
|
|
self.assertEqual(annotationlib.get_annotations(f1, format=1), {"a": int})
|
|
|
|
fwd = annotationlib.ForwardRef("undefined")
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(f2, format=Format.FORWARDREF),
|
|
{"a": fwd},
|
|
)
|
|
self.assertEqual(annotationlib.get_annotations(f2, format=2), {"a": fwd})
|
|
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(f1, format=Format.STRING),
|
|
{"a": "int"},
|
|
)
|
|
self.assertEqual(annotationlib.get_annotations(f1, format=3), {"a": "int"})
|
|
|
|
with self.assertRaises(ValueError):
|
|
annotationlib.get_annotations(f1, format=0)
|
|
|
|
with self.assertRaises(ValueError):
|
|
annotationlib.get_annotations(f1, format=4)
|
|
|
|
def test_custom_object_with_annotations(self):
|
|
class C:
|
|
def __init__(self):
|
|
self.__annotations__ = {"x": int, "y": str}
|
|
|
|
self.assertEqual(annotationlib.get_annotations(C()), {"x": int, "y": str})
|
|
|
|
def test_custom_format_eval_str(self):
|
|
def foo():
|
|
pass
|
|
|
|
with self.assertRaises(ValueError):
|
|
annotationlib.get_annotations(foo, format=Format.FORWARDREF, eval_str=True)
|
|
annotationlib.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(
|
|
annotationlib.get_annotations(foo, format=format),
|
|
{"a": int, "b": str},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(foo, format=Format.STRING),
|
|
{"a": "int", "b": "str"},
|
|
)
|
|
|
|
foo.__annotations__ = {"a": "foo", "b": "str"}
|
|
for format in Format:
|
|
with self.subTest(format=format):
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(foo, format=format),
|
|
{"a": "foo", "b": "str"},
|
|
)
|
|
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(foo, eval_str=True, locals=locals()),
|
|
{"a": foo, "b": str},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.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(
|
|
annotationlib.get_annotations(isa, **kwargs), {"a": int, "b": str}
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.MyClass, **kwargs),
|
|
{"a": int, "b": str},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.function, **kwargs),
|
|
{"a": int, "b": str, "return": isa.MyClass},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.function2, **kwargs),
|
|
{"a": int, "b": "str", "c": isa.MyClass, "return": isa.MyClass},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.function3, **kwargs),
|
|
{"a": "int", "b": "str", "c": "MyClass"},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(annotationlib, **kwargs), {}
|
|
) # annotations module has no annotations
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.UnannotatedClass, **kwargs), {}
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.unannotated_function, **kwargs),
|
|
{},
|
|
)
|
|
|
|
for kwargs in [
|
|
{"eval_str": True},
|
|
{"format": Format.VALUE, "eval_str": True},
|
|
]:
|
|
with self.subTest(**kwargs):
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa, **kwargs), {"a": int, "b": str}
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.MyClass, **kwargs),
|
|
{"a": int, "b": str},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.function, **kwargs),
|
|
{"a": int, "b": str, "return": isa.MyClass},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.function2, **kwargs),
|
|
{"a": int, "b": str, "c": isa.MyClass, "return": isa.MyClass},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.function3, **kwargs),
|
|
{"a": int, "b": str, "c": isa.MyClass},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(annotationlib, **kwargs), {}
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.UnannotatedClass, **kwargs), {}
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.unannotated_function, **kwargs),
|
|
{},
|
|
)
|
|
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa, format=Format.STRING),
|
|
{"a": "int", "b": "str"},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.MyClass, format=Format.STRING),
|
|
{"a": "int", "b": "str"},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.function, format=Format.STRING),
|
|
{"a": "int", "b": "str", "return": "MyClass"},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.function2, format=Format.STRING),
|
|
{"a": "int", "b": "str", "c": "MyClass", "return": "MyClass"},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.function3, format=Format.STRING),
|
|
{"a": "int", "b": "str", "c": "MyClass"},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(annotationlib, format=Format.STRING),
|
|
{},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.UnannotatedClass, format=Format.STRING),
|
|
{},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.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(
|
|
annotationlib.get_annotations(wrapped),
|
|
{"a": int, "b": str, "return": isa.MyClass},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(wrapped, format=Format.FORWARDREF),
|
|
{"a": int, "b": str, "return": isa.MyClass},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(wrapped, format=Format.STRING),
|
|
{"a": "int", "b": "str", "return": "MyClass"},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(wrapped, eval_str=True),
|
|
{"a": int, "b": str, "return": isa.MyClass},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.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(
|
|
annotationlib.get_annotations(isa, **kwargs),
|
|
{"a": "int", "b": "str"},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.MyClass, **kwargs),
|
|
{"a": "int", "b": "str"},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.function, **kwargs),
|
|
{"a": "int", "b": "str", "return": "MyClass"},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.function2, **kwargs),
|
|
{"a": "int", "b": "'str'", "c": "MyClass", "return": "MyClass"},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.function3, **kwargs),
|
|
{"a": "'int'", "b": "'str'", "c": "'MyClass'"},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.UnannotatedClass, **kwargs), {}
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.unannotated_function, **kwargs),
|
|
{},
|
|
)
|
|
|
|
for kwargs in [
|
|
{"eval_str": True},
|
|
{"format": Format.VALUE, "eval_str": True},
|
|
]:
|
|
with self.subTest(**kwargs):
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa, **kwargs), {"a": int, "b": str}
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.MyClass, **kwargs),
|
|
{"a": int, "b": str},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.function, **kwargs),
|
|
{"a": int, "b": str, "return": isa.MyClass},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.function2, **kwargs),
|
|
{"a": int, "b": "str", "c": isa.MyClass, "return": isa.MyClass},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.function3, **kwargs),
|
|
{"a": "int", "b": "str", "c": "MyClass"},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.UnannotatedClass, **kwargs), {}
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(isa.unannotated_function, **kwargs),
|
|
{},
|
|
)
|
|
|
|
def test_stringized_annotations_in_empty_module(self):
|
|
isa2 = inspect_stringized_annotations_2
|
|
self.assertEqual(annotationlib.get_annotations(isa2), {})
|
|
self.assertEqual(annotationlib.get_annotations(isa2, eval_str=True), {})
|
|
self.assertEqual(annotationlib.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(
|
|
annotationlib.get_annotations(wrapped),
|
|
{"a": "int", "b": "str", "return": "MyClass"},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(wrapped, eval_str=True),
|
|
{"a": int, "b": str, "return": isa.MyClass},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.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(
|
|
annotationlib.get_annotations(isa.MyClassWithLocalAnnotations),
|
|
{"x": "mytype"},
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(
|
|
isa.MyClassWithLocalAnnotations, eval_str=True
|
|
),
|
|
{"x": int},
|
|
)
|
|
|
|
def test_modify_annotations(self):
|
|
def f(x: int):
|
|
pass
|
|
|
|
self.assertEqual(annotationlib.get_annotations(f), {"x": int})
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(f, format=Format.FORWARDREF),
|
|
{"x": int},
|
|
)
|
|
|
|
f.__annotations__["x"] = str
|
|
# The modification is reflected in VALUE (the default)
|
|
self.assertEqual(annotationlib.get_annotations(f), {"x": str})
|
|
# ... and also in FORWARDREF, which tries __annotations__ if available
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(f, format=Format.FORWARDREF),
|
|
{"x": str},
|
|
)
|
|
# ... but not in STRING which always uses __annotate__
|
|
self.assertEqual(
|
|
annotationlib.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:
|
|
with (
|
|
self.subTest(format=format),
|
|
self.assertRaisesRegex(
|
|
ValueError, r".*__annotations__ is neither a dict nor None"
|
|
),
|
|
):
|
|
annotationlib.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(
|
|
annotationlib.get_annotations(ha, format=Format.VALUE), {"x": int}
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(ha, format=Format.FORWARDREF), {"x": int}
|
|
)
|
|
|
|
self.assertEqual(
|
|
annotationlib.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):
|
|
annotationlib.get_annotations(hra, format=Format.VALUE)
|
|
|
|
with self.assertRaises(NameError):
|
|
annotationlib.get_annotations(hra, format=Format.FORWARDREF)
|
|
|
|
undefined = float
|
|
self.assertEqual(
|
|
annotationlib.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(
|
|
annotationlib.get_annotations(hb, format=Format.VALUE), {"x": int}
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(hb, format=Format.FORWARDREF), {"x": int}
|
|
)
|
|
self.assertEqual(
|
|
annotationlib.get_annotations(hb, format=Format.STRING), {"x": str}
|
|
)
|
|
|
|
def test_pep695_generic_class_with_future_annotations(self):
|
|
ann_module695 = inspect_stringized_annotations_pep695
|
|
A_annotations = annotationlib.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 = annotationlib.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 = annotationlib.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 = annotationlib.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(
|
|
annotationlib.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 = annotationlib.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(
|
|
annotationlib.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(
|
|
annotationlib.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__),
|
|
)
|
|
|
|
|
|
class TestCallEvaluateFunction(unittest.TestCase):
|
|
def test_evaluation(self):
|
|
def evaluate(format, exc=NotImplementedError):
|
|
if format != 1:
|
|
raise exc
|
|
return undefined
|
|
|
|
with self.assertRaises(NameError):
|
|
annotationlib.call_evaluate_function(evaluate, Format.VALUE)
|
|
self.assertEqual(
|
|
annotationlib.call_evaluate_function(evaluate, Format.FORWARDREF),
|
|
annotationlib.ForwardRef("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(get_annotate_function(Meta)(Format.VALUE), {"a": int})
|
|
|
|
self.assertEqual(get_annotations(X), {})
|
|
self.assertIs(get_annotate_function(X), None)
|
|
|
|
self.assertEqual(get_annotations(Y), {"b": float})
|
|
self.assertEqual(get_annotate_function(Y)(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(get_annotate_function(Meta), None)
|
|
|
|
self.assertEqual(get_annotations(Y), {})
|
|
self.assertIs(get_annotate_function(Y), None)
|
|
|
|
self.assertEqual(get_annotations(X), {"a": str})
|
|
self.assertEqual(get_annotate_function(X)(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 = get_annotate_function(c)
|
|
if c.expected_annotations:
|
|
self.assertEqual(
|
|
annotate_func(Format.VALUE), c.expected_annotations
|
|
)
|
|
else:
|
|
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
|
|
|
|
self.assertIsNone(get_annotate_function(C))
|
|
|
|
D = type("D", (), {})
|
|
self.assertIsNone(get_annotate_function(D))
|
|
|
|
def test_annotated_class(self):
|
|
class C:
|
|
a: int
|
|
|
|
self.assertEqual(get_annotate_function(C)(Format.VALUE), {"a": int})
|
|
|
|
|
|
class TestToSource(unittest.TestCase):
|
|
def test_value_to_string(self):
|
|
self.assertEqual(value_to_string(int), "int")
|
|
self.assertEqual(value_to_string(MyClass), "test.test_annotationlib.MyClass")
|
|
self.assertEqual(value_to_string(len), "len")
|
|
self.assertEqual(value_to_string(value_to_string), "value_to_string")
|
|
self.assertEqual(value_to_string(times_three), "times_three")
|
|
self.assertEqual(value_to_string(...), "...")
|
|
self.assertEqual(value_to_string(None), "None")
|
|
self.assertEqual(value_to_string(1), "1")
|
|
self.assertEqual(value_to_string("1"), "'1'")
|
|
self.assertEqual(value_to_string(Format.VALUE), repr(Format.VALUE))
|
|
self.assertEqual(value_to_string(MyClass()), "my repr")
|
|
|
|
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 TestAnnotationLib(unittest.TestCase):
|
|
def test__all__(self):
|
|
support.check__all__(self, annotationlib)
|