diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 5ec91cddaa3e109..f474e600b7c15c6 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -854,6 +854,9 @@ def _get_field(cls, a_name, a_type, default_kw_only): # For the complete discussion, see https://bugs.python.org/issue33453 if isinstance(a_type, str): a_type_annotation = _get_type_from_annotation(a_type, cls) + elif isinstance(a_type, annotationlib.ForwardRef): + # An unresolved lazy import can defer the whole annotation. + a_type_annotation = _get_type_from_annotation(a_type.__forward_arg__, cls) else: a_type_annotation = a_type diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 2b4defe35f8a33d..c8a5b87f5fdc59b 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -4530,6 +4530,32 @@ 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)) + self.assertIsInstance(cls.__dataclass_fields__['value'].type, + annotationlib.ForwardRef) + + 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 diff --git a/Lib/test/test_dataclasses/dataclass_lazy_initvar.py b/Lib/test/test_dataclasses/dataclass_lazy_initvar.py new file mode 100644 index 000000000000000..759cb70d1bf12a3 --- /dev/null +++ b/Lib/test/test_dataclasses/dataclass_lazy_initvar.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass + +from test.test_dataclasses.dataclass_lazy_initvar_other import ( + AliasedInitVar, DeferredClassVar, DeferredField, DirectInitVar, + QualifiedInitVar, +) + +__all__ = [ + 'A', 'AliasedInitVar', 'DeferredClassVar', 'DeferredField', + 'DirectInitVar', 'QualifiedInitVar', +] + + +@dataclass +class A: + pass diff --git a/Lib/test/test_dataclasses/dataclass_lazy_initvar_other.py b/Lib/test/test_dataclasses/dataclass_lazy_initvar_other.py new file mode 100644 index 000000000000000..97c7f6dac61bd5c --- /dev/null +++ b/Lib/test/test_dataclasses/dataclass_lazy_initvar_other.py @@ -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 diff --git a/Misc/NEWS.d/next/Library/2026-10-25-10-12-00.gh-issue-160665.3jLz8x.rst b/Misc/NEWS.d/next/Library/2026-10-25-10-12-00.gh-issue-160665.3jLz8x.rst new file mode 100644 index 000000000000000..5e2c4d7b92efcf0 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-25-10-12-00.gh-issue-160665.3jLz8x.rst @@ -0,0 +1,3 @@ +Fix :func:`dataclasses.dataclass` handling of :class:`~dataclasses.InitVar` and +:data:`~typing.ClassVar` annotations when a lazy import cannot yet be resolved +during a circular import.