From 92e2f769eaa1d308873ef977a2f4fd8e48a4a35f Mon Sep 17 00:00:00 2001 From: Dmitrii Zherbin Date: Fri, 16 May 2025 01:37:52 +0300 Subject: [PATCH 1/6] 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/6] 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/6] 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__ From b7d6f76019366e3138c17ca44433f6666d9bb83a Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Fri, 24 Oct 2025 15:24:56 +0400 Subject: [PATCH 4/6] fix: further improve detection of ClassVar in dataclass' string annotations --- Lib/dataclasses.py | 46 ++++++------------- Lib/test/test_dataclasses/__init__.py | 3 ++ .../dataclass_module_3_str.py | 2 +- .../test_dataclasses/dataclass_module_4.py | 37 +++++++++++++++ .../dataclass_module_4_str.py | 37 +++++++++++++++ ...-05-16-01-43-58.gh-issue-133956.5kWDYd.rst | 5 +- 6 files changed, 97 insertions(+), 33 deletions(-) create mode 100644 Lib/test/test_dataclasses/dataclass_module_4.py create mode 100644 Lib/test/test_dataclasses/dataclass_module_4_str.py diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 8ed81c1fa8d468..7ae5cd03ac1fe4 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -711,22 +711,20 @@ def _is_kw_only(a_type, dataclasses): return a_type is dataclasses.KW_ONLY -def _is_type(annotation, cls, a_module, a_type, is_type_predicate): - # Given a type annotation string, does it refer to a_type in - # a_module? For example, when checking that annotation denotes a - # ClassVar, then a_module is typing, and a_type is - # typing.ClassVar. +def _is_type(annotation, cls, is_type_predicate, *is_type_predicate_args): + # Loosely parse a string annotation and pass the result to is_type_predicate, + # along with any additional arguments it might require. - # It's possible to look up a_module given a_type, but it involves - # looking in sys.modules (again!), and seems like a waste since - # the caller already knows a_module. + # We can't perform a full type hint evaluation at the point where @dataclass + # was invoked because class's module is not fully initialized yet. So we resort + # to parsing string annotation using regexp, and extracting a type before + # the first square bracket. # - annotation is a string type annotation # - cls is the class that this annotation was found in - # - a_module is the module we want to match - # - a_type is the type in that module we want to match - # - is_type_predicate is a function called with (obj, a_module) + # - is_type_predicate is a function called with (obj, *is_type_predicate_args) # that determines if obj is of the desired type. + # - is_type_predicate_args is additional arguments forwarded to is_type_predicate # Since this test does not do a local namespace lookup (and # instead only a module (global) lookup), there are some things it @@ -756,30 +754,19 @@ def _is_type(annotation, cls, a_module, a_type, is_type_predicate): 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__ + ns = sys.modules.get(cls.__module__) else: # Look up module_name in the class's module. cls_module = sys.modules.get(cls.__module__) - if not cls_module: - return False + ns = cls_module.__dict__.get(module_name) - 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) + return is_type_predicate(getattr(ns, type_name, None), *is_type_predicate_args) def _get_field(cls, a_name, a_type, default_kw_only): @@ -826,8 +813,7 @@ def _get_field(cls, a_name, a_type, default_kw_only): if typing: if (_is_classvar(a_type, typing) or (isinstance(f.type, str) - and _is_type(f.type, cls, typing, typing.ClassVar, - _is_classvar))): + and _is_type(f.type, cls, _is_classvar, typing))): f._field_type = _FIELD_CLASSVAR # If the type is InitVar, or if it's a matching string annotation, @@ -838,8 +824,7 @@ def _get_field(cls, a_name, a_type, default_kw_only): dataclasses = sys.modules[__name__] if (_is_initvar(a_type, dataclasses) or (isinstance(f.type, str) - and _is_type(f.type, cls, dataclasses, dataclasses.InitVar, - _is_initvar))): + and _is_type(f.type, cls, _is_initvar, dataclasses))): f._field_type = _FIELD_INITVAR # Validations for individual fields. This is delayed until now, @@ -1012,8 +997,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, # See if this is a marker to change the value of kw_only. if (_is_kw_only(type, dataclasses) or (isinstance(type, str) - and _is_type(type, cls, dataclasses, dataclasses.KW_ONLY, - _is_kw_only))): + and _is_type(type, cls, _is_kw_only, dataclasses))): # Switch the default to kw_only=True, and ignore this # annotation: it's not a real field. if KW_ONLY_seen: diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 1f7b11178295da..7d84380a31d8f4 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -4095,11 +4095,14 @@ def test_classvar_module_level_import(self): 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 + from test.test_dataclasses import dataclass_module_4 + from test.test_dataclasses import dataclass_module_4_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, + dataclass_module_4, dataclass_module_4_str, ): with self.subTest(m=m): # There's a difference in how the ClassVars are diff --git a/Lib/test/test_dataclasses/dataclass_module_3_str.py b/Lib/test/test_dataclasses/dataclass_module_3_str.py index 6a9d532fcf52a6..49e5fca61831b6 100644 --- a/Lib/test/test_dataclasses/dataclass_module_3_str.py +++ b/Lib/test/test_dataclasses/dataclass_module_3_str.py @@ -1,7 +1,7 @@ from __future__ import annotations USING_STRINGS = True -# dataclass_module_3.py and dataclass_module_2_str.py are identical +# dataclass_module_3.py and dataclass_module_3_str.py are identical # except only the latter uses string annotations. from dataclasses import dataclass diff --git a/Lib/test/test_dataclasses/dataclass_module_4.py b/Lib/test/test_dataclasses/dataclass_module_4.py new file mode 100644 index 00000000000000..14b1e502f5cd2b --- /dev/null +++ b/Lib/test/test_dataclasses/dataclass_module_4.py @@ -0,0 +1,37 @@ +#from __future__ import annotations +USING_STRINGS = False + +# dataclass_module_4.py and dataclass_module_4_str.py are identical +# except only the latter uses string annotations. + +from dataclasses import dataclass +import dataclasses +import typing + +class TypingProxy: + ClassVar = typing.ClassVar + InitVar = dataclasses.InitVar + +T_CV2 = TypingProxy.ClassVar[int] +T_CV3 = TypingProxy.ClassVar + +T_IV2 = TypingProxy.InitVar[int] +T_IV3 = TypingProxy.InitVar + +@dataclass +class CV: + T_CV4 = TypingProxy.ClassVar + cv0: TypingProxy.ClassVar[int] = 20 + cv1: TypingProxy.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 = TypingProxy.InitVar + iv0: TypingProxy.InitVar[int] + iv1: TypingProxy.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_4_str.py b/Lib/test/test_dataclasses/dataclass_module_4_str.py new file mode 100644 index 00000000000000..2b54282dfdf0df --- /dev/null +++ b/Lib/test/test_dataclasses/dataclass_module_4_str.py @@ -0,0 +1,37 @@ +from __future__ import annotations +USING_STRINGS = True + +# dataclass_module_4.py and dataclass_module_4_str.py are identical +# except only the latter uses string annotations. + +from dataclasses import dataclass +import dataclasses +import typing + +class TypingProxy: + ClassVar = typing.ClassVar + InitVar = dataclasses.InitVar + +T_CV2 = TypingProxy.ClassVar[int] +T_CV3 = TypingProxy.ClassVar + +T_IV2 = TypingProxy.InitVar[int] +T_IV3 = TypingProxy.InitVar + +@dataclass +class CV: + T_CV4 = TypingProxy.ClassVar + cv0: TypingProxy.ClassVar[int] = 20 + cv1: TypingProxy.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 = TypingProxy.InitVar + iv0: TypingProxy.InitVar[int] + iv1: TypingProxy.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 index a5949ca176cce0..5923e12d55964c 100644 --- 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 @@ -1 +1,4 @@ -Fix bug where ``ClassVar`` string annotation in :func:`@dataclass ` caused incorrect __init__ generation +Fix bug where :func:`@dataclass ` +wouldn't detect ``ClassVar`` fields +if ``ClassVar`` was re-exported from a module +other than :mod:`typing`. From 2f64527c75ce0b42ffe2072df31281f43b4b05f2 Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Fri, 24 Oct 2025 16:11:12 +0400 Subject: [PATCH 5/6] fix: support nesting annotations when detecting `ClassVar` fields in dataclasses --- Lib/dataclasses.py | 52 ++++++++----------- Lib/test/test_dataclasses/__init__.py | 8 +++ .../test_dataclasses/dataclass_module_4.py | 25 ++++----- .../dataclass_module_4_str.py | 25 ++++----- 4 files changed, 57 insertions(+), 53 deletions(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 7ae5cd03ac1fe4..decd97129460eb 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -219,7 +219,7 @@ def __repr__(self): # String regex that string annotations for ClassVar or InitVar must match. # Allows "identifier.identifier[" or "identifier[". # https://bugs.python.org/issue33453 for details. -_MODULE_IDENTIFIER_RE = re.compile(r'^(?:\s*(\w+)\s*\.)?\s*(\w+)') +_MODULE_IDENTIFIER_RE = re.compile(r'^\s*(\w+(?:\s*\.\s*\w+)*)') # Atomic immutable types which don't require any recursive handling and for which deepcopy # returns the same object. We can provide a fast-path for these types in asdict and astuple. @@ -711,9 +711,8 @@ def _is_kw_only(a_type, dataclasses): return a_type is dataclasses.KW_ONLY -def _is_type(annotation, cls, is_type_predicate, *is_type_predicate_args): - # Loosely parse a string annotation and pass the result to is_type_predicate, - # along with any additional arguments it might require. +def _get_type_from_annotation(annotation, cls): + # Loosely parse a string annotation and return its type. # We can't perform a full type hint evaluation at the point where @dataclass # was invoked because class's module is not fully initialized yet. So we resort @@ -722,9 +721,6 @@ def _is_type(annotation, cls, is_type_predicate, *is_type_predicate_args): # - annotation is a string type annotation # - cls is the class that this annotation was found in - # - is_type_predicate is a function called with (obj, *is_type_predicate_args) - # that determines if obj is of the desired type. - # - is_type_predicate_args is additional arguments forwarded to is_type_predicate # Since this test does not do a local namespace lookup (and # instead only a module (global) lookup), there are some things it @@ -752,21 +748,17 @@ def _is_type(annotation, cls, is_type_predicate, *is_type_predicate_args): match = _MODULE_IDENTIFIER_RE.match(annotation) if not match: - return False - - module_name = match.group(1) - type_name = match.group(2) + return None - if not module_name: - # No module name, assume the class's module did - # "from dataclasses import InitVar". - ns = sys.modules.get(cls.__module__) - else: - # Look up module_name in the class's module. - cls_module = sys.modules.get(cls.__module__) - ns = cls_module.__dict__.get(module_name) + # Note: _MODULE_IDENTIFIER_RE guarantees that path is non-empty + path = match.group(1).split(".") + root = sys.modules.get(cls.__module__) + for path_item in path: + root = getattr(root, path_item.strip(), None) + if root is None: + return None - return is_type_predicate(getattr(ns, type_name, None), *is_type_predicate_args) + return root def _get_field(cls, a_name, a_type, default_kw_only): @@ -804,6 +796,10 @@ def _get_field(cls, a_name, a_type, default_kw_only): # is actually of the correct type. # 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) + else: + a_type_annotation = a_type # If typing has not been imported, then it's impossible for any # annotation to be a ClassVar. So, only look for ClassVar if @@ -811,9 +807,7 @@ def _get_field(cls, a_name, a_type, default_kw_only): # module). typing = sys.modules.get('typing') if typing: - if (_is_classvar(a_type, typing) - or (isinstance(f.type, str) - and _is_type(f.type, cls, _is_classvar, typing))): + if _is_classvar(a_type_annotation, typing): f._field_type = _FIELD_CLASSVAR # If the type is InitVar, or if it's a matching string annotation, @@ -822,9 +816,7 @@ def _get_field(cls, a_name, a_type, default_kw_only): # The module we're checking against is the module we're # currently in (dataclasses.py). dataclasses = sys.modules[__name__] - if (_is_initvar(a_type, dataclasses) - or (isinstance(f.type, str) - and _is_type(f.type, cls, _is_initvar, dataclasses))): + if _is_initvar(a_type_annotation, dataclasses): f._field_type = _FIELD_INITVAR # Validations for individual fields. This is delayed until now, @@ -995,9 +987,11 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, dataclasses = sys.modules[__name__] for name, type in cls_annotations.items(): # See if this is a marker to change the value of kw_only. - if (_is_kw_only(type, dataclasses) - or (isinstance(type, str) - and _is_type(type, cls, _is_kw_only, dataclasses))): + if isinstance(type, str): + a_type_annotation = _get_type_from_annotation(type, cls) + else: + a_type_annotation = type + if _is_kw_only(a_type_annotation, dataclasses): # Switch the default to kw_only=True, and ignore this # annotation: it's not a real field. if KW_ONLY_seen: diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 7d84380a31d8f4..c678ea94761420 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -4407,6 +4407,14 @@ def custom_dataclass(cls, *args, **kwargs): self.assertEqual(c.x, 10) self.assertEqual(c.__custom__, True) + def test_empty_annotation_string(self): + @dataclass + class DataclassWithEmptyTypeAnnotation: + x: "" + + c = DataclassWithEmptyTypeAnnotation(10) + self.assertEqual(c.x, 10) + class TestReplace(unittest.TestCase): def test(self): diff --git a/Lib/test/test_dataclasses/dataclass_module_4.py b/Lib/test/test_dataclasses/dataclass_module_4.py index 14b1e502f5cd2b..7e0c8a18356590 100644 --- a/Lib/test/test_dataclasses/dataclass_module_4.py +++ b/Lib/test/test_dataclasses/dataclass_module_4.py @@ -9,29 +9,30 @@ import typing class TypingProxy: - ClassVar = typing.ClassVar - InitVar = dataclasses.InitVar + class Nested: + ClassVar = typing.ClassVar + InitVar = dataclasses.InitVar -T_CV2 = TypingProxy.ClassVar[int] -T_CV3 = TypingProxy.ClassVar +T_CV2 = TypingProxy.Nested.ClassVar[int] +T_CV3 = TypingProxy.Nested.ClassVar -T_IV2 = TypingProxy.InitVar[int] -T_IV3 = TypingProxy.InitVar +T_IV2 = TypingProxy.Nested.InitVar[int] +T_IV3 = TypingProxy.Nested.InitVar @dataclass class CV: - T_CV4 = TypingProxy.ClassVar - cv0: TypingProxy.ClassVar[int] = 20 - cv1: TypingProxy.ClassVar = 30 + T_CV4 = TypingProxy.Nested.ClassVar + cv0: TypingProxy.Nested.ClassVar[int] = 20 + cv1: TypingProxy.Nested.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 = TypingProxy.InitVar - iv0: TypingProxy.InitVar[int] - iv1: TypingProxy.InitVar + T_IV4 = TypingProxy.Nested.InitVar + iv0: TypingProxy.Nested.InitVar[int] + iv1: TypingProxy.Nested.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_4_str.py b/Lib/test/test_dataclasses/dataclass_module_4_str.py index 2b54282dfdf0df..876f3dcf7c88fa 100644 --- a/Lib/test/test_dataclasses/dataclass_module_4_str.py +++ b/Lib/test/test_dataclasses/dataclass_module_4_str.py @@ -9,29 +9,30 @@ import typing class TypingProxy: - ClassVar = typing.ClassVar - InitVar = dataclasses.InitVar + class Nested: + ClassVar = typing.ClassVar + InitVar = dataclasses.InitVar -T_CV2 = TypingProxy.ClassVar[int] -T_CV3 = TypingProxy.ClassVar +T_CV2 = TypingProxy.Nested.ClassVar[int] +T_CV3 = TypingProxy.Nested.ClassVar -T_IV2 = TypingProxy.InitVar[int] -T_IV3 = TypingProxy.InitVar +T_IV2 = TypingProxy.Nested.InitVar[int] +T_IV3 = TypingProxy.Nested.InitVar @dataclass class CV: - T_CV4 = TypingProxy.ClassVar - cv0: TypingProxy.ClassVar[int] = 20 - cv1: TypingProxy.ClassVar = 30 + T_CV4 = TypingProxy.Nested.ClassVar + cv0: TypingProxy.Nested.ClassVar[int] = 20 + cv1: TypingProxy.Nested.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 = TypingProxy.InitVar - iv0: TypingProxy.InitVar[int] - iv1: TypingProxy.InitVar + T_IV4 = TypingProxy.Nested.InitVar + iv0: TypingProxy.Nested.InitVar[int] + iv1: TypingProxy.Nested.InitVar iv2: T_IV2 iv3: T_IV3 not_iv4: T_IV4 # When using string annotations, this field is not recognized as an InitVar. From 8a855b41a3e26cf1a72f341aaa5118129215cd72 Mon Sep 17 00:00:00 2001 From: Tamika Nomara Date: Fri, 24 Oct 2025 16:38:59 +0400 Subject: [PATCH 6/6] chore: fix lint error --- Lib/test/test_dataclasses/_types_proxy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_dataclasses/_types_proxy.py b/Lib/test/test_dataclasses/_types_proxy.py index bedfe38b48133e..f4aaeef7aec59d 100644 --- a/Lib/test/test_dataclasses/_types_proxy.py +++ b/Lib/test/test_dataclasses/_types_proxy.py @@ -4,3 +4,5 @@ # https://github.com/python/cpython/issues/133956 from typing import ClassVar from dataclasses import InitVar + +__all__ = ["ClassVar", "InitVar"]