Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Lib/annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,17 @@ def __init__(self, namespace, *, globals=None, owner=None, is_class=False, forma
self.next_id = 1
self.format = format

def __getitem__(self, key):
value = super().__getitem__(key)
if isinstance(value, types.LazyImportType):
try:
return value.resolve()
except Exception:
# Treat an unresolved lazy import like an undefined name,
# so the rest of the annotation can still be evaluated.
return self.__missing__(key)
return value

def __missing__(self, key):
fwdref = _Stringifier(
key,
Expand Down
79 changes: 79 additions & 0 deletions Lib/test/test_annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -2334,6 +2334,85 @@ class B:
with self.assertRaisesRegex(ValueError, "always fails to import"):
annos["b"].evaluate(format=Format.VALUE)

def test_get_annotations_subscripted_lazy_import(self):
module = types.ModuleType("annotationlib_lazy_import_test")
ns = vars(module)
exec(textwrap.dedent("""
lazy from test.test_lazy_import.data.basic2 import x
lazy from test.test_lazy_import.data.broken_module import y

a: list[y]
b: dict[str, list[y]]
c: list[x]
d: list[undefined]

class C:
a: list[y]
b: dict[str, list[y]]
c: list[x]
d: list[undefined]

def f(a: list[y], b: dict[str, list[y]], c: list[x],
d: list[undefined]):
pass
"""), ns)
self.addCleanup(import_helper.unload,
"test.test_lazy_import.data.basic2")
self.addCleanup(import_helper.unload,
"test.test_lazy_import.data.broken_module")
for owner in (module, ns["C"], ns["f"]):
with self.subTest(owner=owner):
annos = get_annotations(owner, format=Format.FORWARDREF)
self.assertIsInstance(annos["a"], types.GenericAlias)
self.assertIs(annos["a"].__origin__, list)
ref, = annos["a"].__args__
self.assertIsInstance(ref, ForwardRef)
self.assertEqual(ref.__forward_arg__, "y")
self.assertEqual(annos["b"], dict[str, list[ref]])
self.assertEqual(annos["c"], list[42])
unknown, = annos["d"].__args__
self.assertIsInstance(unknown, ForwardRef)
self.assertEqual(unknown.__forward_arg__, "undefined")
self.assertEqual(get_annotations(owner, format=Format.STRING), {
"a": "list[y]", "b": "dict[str, list[y]]",
"c": "list[x]", "d": "list[undefined]",
})
with self.assertRaisesRegex(ValueError, "always fails to import"):
get_annotations(owner, format=Format.VALUE)

def test_evaluate_subscripted_lazy_import(self):
ns = {}
exec("lazy from test.test_lazy_import.data.broken_module import y", ns)
self.addCleanup(import_helper.unload,
"test.test_lazy_import.data.broken_module")
for scope in ("globals", "locals"):
with self.subTest(scope=scope):
ref = ForwardRef("list[y]")
value = ref.evaluate(**{scope: ns}, format=Format.FORWARDREF)
self.assertIsInstance(value, types.GenericAlias)
self.assertIs(value.__origin__, list)
inner, = value.__args__
self.assertIsInstance(inner, ForwardRef)
self.assertEqual(inner.__forward_arg__, "y")
with self.assertRaisesRegex(ValueError, "always fails to import"):
ref.evaluate(**{scope: ns}, format=Format.VALUE)

def test_unreferenced_lazy_import(self):
name = "test.test_lazy_import.data.basic2"
with import_helper.CleanImport(name):
ns = {}
exec(textwrap.dedent("""
lazy from test.test_lazy_import.data.basic2 import x
class C:
a: undefined
b: int
"""), ns)
self.assertNotIn(name, sys.modules)
annos = get_annotations(ns["C"], format=Format.FORWARDREF)
self.assertEqual(annos["b"], int)
self.assertEqual(annos["a"].__forward_arg__, "undefined")
self.assertNotIn(name, sys.modules)

def test_evaluate_notimplemented_format(self):
class C:
x: alias
Expand Down
28 changes: 28 additions & 0 deletions Lib/test/test_dataclasses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4530,6 +4530,34 @@ def test_lazy_import_annotations(self):
self.assertEqual([f.name for f in fields(m.C)], ['x', 'y'])
self.assertNotIsInstance(vars(m)['t'], types.LazyImportType)

def test_lazy_import_forwardref_annotations(self):
m = import_helper.import_fresh_module(
'test.test_dataclasses.dataclass_lazy_initvar',
fresh=['test.test_dataclasses.dataclass_lazy_initvar_other'],
)
self.assertIsNotNone(m)
value = m.A()
for cls in (m.DirectInitVar, m.QualifiedInitVar, m.AliasedInitVar):
with self.subTest(cls=cls):
instance = cls(value)
self.assertIs(instance.received, value)
self.assertEqual(fields(cls), ())
self.assertNotIn('value', vars(instance))
field_type = cls.__dataclass_fields__['value'].type
self.assertIsInstance(field_type, InitVar)
self.assertIsInstance(field_type.type, annotationlib.ForwardRef)
self.assertEqual(field_type.type.__forward_arg__, 'A')

with self.subTest(cls=m.DeferredClassVar):
self.assertEqual(fields(m.DeferredClassVar), ())
self.assertEqual(vars(m.DeferredClassVar()), {})

with self.subTest(cls=m.DeferredField):
self.assertEqual([f.name for f in fields(m.DeferredField)], ['value'])
self.assertIs(m.DeferredField(value).value, value)
self.assertIsInstance(fields(m.DeferredField)[0].type,
annotationlib.ForwardRef)

def test_text_annotations(self):
from test.test_dataclasses import dataclass_textanno

Expand Down
20 changes: 20 additions & 0 deletions Lib/test/test_dataclasses/dataclass_lazy_initvar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from dataclasses import dataclass

from test.test_dataclasses.dataclass_lazy_initvar_other import (
AliasedInitVar, DeferredClassVar, DeferredField, DirectInitVar,
QualifiedInitVar,
)

# Resolve every circular import before A is defined, even with lazy_imports=all.
_ = (AliasedInitVar, DeferredClassVar, DeferredField, DirectInitVar,
QualifiedInitVar)

__all__ = [
'A', 'AliasedInitVar', 'DeferredClassVar', 'DeferredField',
'DirectInitVar', 'QualifiedInitVar',
]


@dataclass
class A:
pass
40 changes: 40 additions & 0 deletions Lib/test/test_dataclasses/dataclass_lazy_initvar_other.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from dataclasses import dataclass, InitVar
from dataclasses import InitVar as IV
from typing import ClassVar
import dataclasses

lazy from test.test_dataclasses.dataclass_lazy_initvar import A


@dataclass
class DirectInitVar:
value: InitVar[A]

def __post_init__(self, value):
self.received = value


@dataclass
class QualifiedInitVar:
value: dataclasses.InitVar[A]

def __post_init__(self, value):
self.received = value


@dataclass
class AliasedInitVar:
value: IV[A]

def __post_init__(self, value):
self.received = value


@dataclass
class DeferredClassVar:
value: ClassVar[A] = None


@dataclass
class DeferredField:
value: A
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Preserve partially resolved annotations when a lazy import cannot be resolved
in :mod:`annotationlib`'s :attr:`~annotationlib.Format.FORWARDREF` format. This
also fixes :class:`~dataclasses.InitVar` and :data:`~typing.ClassVar` handling
during circular imports.
Loading