From 92e2f769eaa1d308873ef977a2f4fd8e48a4a35f Mon Sep 17 00:00:00 2001 From: Dmitrii Zherbin Date: Fri, 16 May 2025 01:37:52 +0300 Subject: [PATCH 1/3] fix: string annotations ClassVar bug --- Lib/dataclasses.py | 42 ++++++++++++------- Lib/test/test_dataclasses/__init__.py | 10 +++-- Lib/test/test_dataclasses/_types_proxy.py | 6 +++ .../test_dataclasses/dataclass_module_3.py | 32 ++++++++++++++ .../dataclass_module_3_str.py | 32 ++++++++++++++ 5 files changed, 104 insertions(+), 18 deletions(-) create mode 100644 Lib/test/test_dataclasses/_types_proxy.py create mode 100644 Lib/test/test_dataclasses/dataclass_module_3.py create mode 100644 Lib/test/test_dataclasses/dataclass_module_3_str.py diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 86d29df0639184..068639a0ceeb1d 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) + # Consider the case when a_type does not belong + # to the namespace, 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. From bae38af5e9b8cbecb4aae8547b83836a3f2d53b5 Mon Sep 17 00:00:00 2001 From: Dmitrii Zherbin Date: Fri, 16 May 2025 01:50:09 +0300 Subject: [PATCH 2/3] doc: add NEWS entry --- .../next/Library/2025-05-16-01-43-58.gh-issue-133956.5kWDYd.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-05-16-01-43-58.gh-issue-133956.5kWDYd.rst 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 From 6f5c21034412d2b38b85b3087659d0934f9dbca0 Mon Sep 17 00:00:00 2001 From: Dmitrii Zherbin Date: Fri, 16 May 2025 02:07:18 +0300 Subject: [PATCH 3/3] chore: correct comment phrasing --- Lib/dataclasses.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 068639a0ceeb1d..8ed81c1fa8d468 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -773,8 +773,8 @@ def _is_type(annotation, cls, a_module, a_type, is_type_predicate): a_type_module = cls_module.__dict__.get(module_name) if ( isinstance(a_type_module, types.ModuleType) - # Consider the case when a_type does not belong - # to the namespace, e.g. 'dataclasses.ClassVar[int]' + # 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__