diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 86d29df0639184..8ed81c1fa8d468 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -753,21 +753,33 @@ def _is_type(annotation, cls, a_module, a_type, is_type_predicate): # that's defined. It was judged not worth it. match = _MODULE_IDENTIFIER_RE.match(annotation) - if match: - ns = None - module_name = match.group(1) - if not module_name: - # No module name, assume the class's module did - # "from dataclasses import InitVar". - ns = sys.modules.get(cls.__module__).__dict__ - else: - # Look up module_name in the class's module. - module = sys.modules.get(cls.__module__) - if module and module.__dict__.get(module_name) is a_module: - ns = sys.modules.get(a_type.__module__).__dict__ - if ns and is_type_predicate(ns.get(match.group(2)), a_module): - return True - return False + if not match: + return False + + ns = None + module_name = match.group(1) + type_name = match.group(2) + + if not module_name: + # No module name, assume the class's module did + # "from dataclasses import InitVar". + ns = sys.modules.get(cls.__module__).__dict__ + else: + # Look up module_name in the class's module. + cls_module = sys.modules.get(cls.__module__) + if not cls_module: + return False + + a_type_module = cls_module.__dict__.get(module_name) + if ( + isinstance(a_type_module, types.ModuleType) + # Handle cases when a_type is not defined in + # the referenced module, e.g. 'dataclasses.ClassVar[int]' + and a_type_module.__dict__.get(type_name) is a_type + ): + ns = sys.modules.get(a_type.__module__).__dict__ + + return ns and is_type_predicate(ns.get(type_name), a_module) def _get_field(cls, a_name, a_type, default_kw_only): diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index ac78f8327b808e..1f7b11178295da 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -4093,10 +4093,14 @@ def test_classvar_module_level_import(self): from test.test_dataclasses import dataclass_module_1_str from test.test_dataclasses import dataclass_module_2 from test.test_dataclasses import dataclass_module_2_str + from test.test_dataclasses import dataclass_module_3 + from test.test_dataclasses import dataclass_module_3_str - for m in (dataclass_module_1, dataclass_module_1_str, - dataclass_module_2, dataclass_module_2_str, - ): + for m in ( + dataclass_module_1, dataclass_module_1_str, + dataclass_module_2, dataclass_module_2_str, + dataclass_module_3, dataclass_module_3_str, + ): with self.subTest(m=m): # There's a difference in how the ClassVars are # interpreted when using string annotations or diff --git a/Lib/test/test_dataclasses/_types_proxy.py b/Lib/test/test_dataclasses/_types_proxy.py new file mode 100644 index 00000000000000..bedfe38b48133e --- /dev/null +++ b/Lib/test/test_dataclasses/_types_proxy.py @@ -0,0 +1,6 @@ +# We need this to test a case when a type +# is imported via some other package, +# like ClassVar from typing_extensions instead of typing. +# https://github.com/python/cpython/issues/133956 +from typing import ClassVar +from dataclasses import InitVar diff --git a/Lib/test/test_dataclasses/dataclass_module_3.py b/Lib/test/test_dataclasses/dataclass_module_3.py new file mode 100644 index 00000000000000..74abc091f35acd --- /dev/null +++ b/Lib/test/test_dataclasses/dataclass_module_3.py @@ -0,0 +1,32 @@ +#from __future__ import annotations +USING_STRINGS = False + +# dataclass_module_3.py and dataclass_module_3_str.py are identical +# except only the latter uses string annotations. + +from dataclasses import dataclass +import test.test_dataclasses._types_proxy as tp + +T_CV2 = tp.ClassVar[int] +T_CV3 = tp.ClassVar + +T_IV2 = tp.InitVar[int] +T_IV3 = tp.InitVar + +@dataclass +class CV: + T_CV4 = tp.ClassVar + cv0: tp.ClassVar[int] = 20 + cv1: tp.ClassVar = 30 + cv2: T_CV2 + cv3: T_CV3 + not_cv4: T_CV4 # When using string annotations, this field is not recognized as a ClassVar. + +@dataclass +class IV: + T_IV4 = tp.InitVar + iv0: tp.InitVar[int] + iv1: tp.InitVar + iv2: T_IV2 + iv3: T_IV3 + not_iv4: T_IV4 # When using string annotations, this field is not recognized as an InitVar. diff --git a/Lib/test/test_dataclasses/dataclass_module_3_str.py b/Lib/test/test_dataclasses/dataclass_module_3_str.py new file mode 100644 index 00000000000000..6a9d532fcf52a6 --- /dev/null +++ b/Lib/test/test_dataclasses/dataclass_module_3_str.py @@ -0,0 +1,32 @@ +from __future__ import annotations +USING_STRINGS = True + +# dataclass_module_3.py and dataclass_module_2_str.py are identical +# except only the latter uses string annotations. + +from dataclasses import dataclass +import test.test_dataclasses._types_proxy as tp + +T_CV2 = tp.ClassVar[int] +T_CV3 = tp.ClassVar + +T_IV2 = tp.InitVar[int] +T_IV3 = tp.InitVar + +@dataclass +class CV: + T_CV4 = tp.ClassVar + cv0: tp.ClassVar[int] = 20 + cv1: tp.ClassVar = 30 + cv2: T_CV2 + cv3: T_CV3 + not_cv4: T_CV4 # When using string annotations, this field is not recognized as a ClassVar. + +@dataclass +class IV: + T_IV4 = tp.InitVar + iv0: tp.InitVar[int] + iv1: tp.InitVar + iv2: T_IV2 + iv3: T_IV3 + not_iv4: T_IV4 # When using string annotations, this field is not recognized as an InitVar. diff --git a/Misc/NEWS.d/next/Library/2025-05-16-01-43-58.gh-issue-133956.5kWDYd.rst b/Misc/NEWS.d/next/Library/2025-05-16-01-43-58.gh-issue-133956.5kWDYd.rst new file mode 100644 index 00000000000000..a5949ca176cce0 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-16-01-43-58.gh-issue-133956.5kWDYd.rst @@ -0,0 +1 @@ +Fix bug where ``ClassVar`` string annotation in :func:`@dataclass ` caused incorrect __init__ generation