Skip to content

Commit 5ecae28

Browse files
committed
fix: support nesting annotations when detecting ClassVar fields in dataclasses
1 parent b7d6f76 commit 5ecae28

File tree

3 files changed

+47
-51
lines changed

3 files changed

+47
-51
lines changed

Lib/dataclasses.py

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ def __repr__(self):
219219
# String regex that string annotations for ClassVar or InitVar must match.
220220
# Allows "identifier.identifier[" or "identifier[".
221221
# https://bugs.python.org/issue33453 for details.
222-
_MODULE_IDENTIFIER_RE = re.compile(r'^(?:\s*(\w+)\s*\.)?\s*(\w+)')
222+
_MODULE_IDENTIFIER_RE = re.compile(r'^\s*(\w+(?:\s*\.\s*\w+)*)')
223223

224224
# Atomic immutable types which don't require any recursive handling and for which deepcopy
225225
# 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):
711711
return a_type is dataclasses.KW_ONLY
712712

713713

714-
def _is_type(annotation, cls, is_type_predicate, *is_type_predicate_args):
715-
# Loosely parse a string annotation and pass the result to is_type_predicate,
716-
# along with any additional arguments it might require.
714+
def _get_type_from_annotation(annotation, cls):
715+
# Loosely parse a string annotation and return its type.
717716

718717
# We can't perform a full type hint evaluation at the point where @dataclass
719718
# 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):
722721

723722
# - annotation is a string type annotation
724723
# - cls is the class that this annotation was found in
725-
# - is_type_predicate is a function called with (obj, *is_type_predicate_args)
726-
# that determines if obj is of the desired type.
727-
# - is_type_predicate_args is additional arguments forwarded to is_type_predicate
728724

729725
# Since this test does not do a local namespace lookup (and
730726
# instead only a module (global) lookup), there are some things it
@@ -754,19 +750,15 @@ def _is_type(annotation, cls, is_type_predicate, *is_type_predicate_args):
754750
if not match:
755751
return False
756752

757-
module_name = match.group(1)
758-
type_name = match.group(2)
753+
*module_path, type_name = match.group(1).split(".")
759754

760-
if not module_name:
761-
# No module name, assume the class's module did
762-
# "from dataclasses import InitVar".
763-
ns = sys.modules.get(cls.__module__)
764-
else:
765-
# Look up module_name in the class's module.
766-
cls_module = sys.modules.get(cls.__module__)
767-
ns = cls_module.__dict__.get(module_name)
755+
ns = sys.modules.get(cls.__module__)
756+
for module_path_item in module_path:
757+
ns = getattr(ns, module_path_item.strip(), None)
758+
if ns is None:
759+
return False
768760

769-
return is_type_predicate(getattr(ns, type_name, None), *is_type_predicate_args)
761+
return getattr(ns, type_name.strip(), None)
770762

771763

772764
def _get_field(cls, a_name, a_type, default_kw_only):
@@ -804,16 +796,18 @@ def _get_field(cls, a_name, a_type, default_kw_only):
804796
# is actually of the correct type.
805797

806798
# For the complete discussion, see https://bugs.python.org/issue33453
799+
if isinstance(a_type, str):
800+
a_type_annotation = _get_type_from_annotation(a_type, cls)
801+
else:
802+
a_type_annotation = a_type
807803

808804
# If typing has not been imported, then it's impossible for any
809805
# annotation to be a ClassVar. So, only look for ClassVar if
810806
# typing has been imported by any module (not necessarily cls's
811807
# module).
812808
typing = sys.modules.get('typing')
813809
if typing:
814-
if (_is_classvar(a_type, typing)
815-
or (isinstance(f.type, str)
816-
and _is_type(f.type, cls, _is_classvar, typing))):
810+
if _is_classvar(a_type_annotation, typing):
817811
f._field_type = _FIELD_CLASSVAR
818812

819813
# 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):
822816
# The module we're checking against is the module we're
823817
# currently in (dataclasses.py).
824818
dataclasses = sys.modules[__name__]
825-
if (_is_initvar(a_type, dataclasses)
826-
or (isinstance(f.type, str)
827-
and _is_type(f.type, cls, _is_initvar, dataclasses))):
819+
if _is_initvar(a_type_annotation, dataclasses):
828820
f._field_type = _FIELD_INITVAR
829821

830822
# Validations for individual fields. This is delayed until now,
@@ -995,9 +987,11 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
995987
dataclasses = sys.modules[__name__]
996988
for name, type in cls_annotations.items():
997989
# See if this is a marker to change the value of kw_only.
998-
if (_is_kw_only(type, dataclasses)
999-
or (isinstance(type, str)
1000-
and _is_type(type, cls, _is_kw_only, dataclasses))):
990+
if isinstance(type, str):
991+
a_type_annotation = _get_type_from_annotation(type, cls)
992+
else:
993+
a_type_annotation = type
994+
if _is_kw_only(a_type_annotation, dataclasses):
1001995
# Switch the default to kw_only=True, and ignore this
1002996
# annotation: it's not a real field.
1003997
if KW_ONLY_seen:

Lib/test/test_dataclasses/dataclass_module_4.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,30 @@
99
import typing
1010

1111
class TypingProxy:
12-
ClassVar = typing.ClassVar
13-
InitVar = dataclasses.InitVar
12+
class Nested:
13+
ClassVar = typing.ClassVar
14+
InitVar = dataclasses.InitVar
1415

15-
T_CV2 = TypingProxy.ClassVar[int]
16-
T_CV3 = TypingProxy.ClassVar
16+
T_CV2 = TypingProxy.Nested.ClassVar[int]
17+
T_CV3 = TypingProxy.Nested.ClassVar
1718

18-
T_IV2 = TypingProxy.InitVar[int]
19-
T_IV3 = TypingProxy.InitVar
19+
T_IV2 = TypingProxy.Nested.InitVar[int]
20+
T_IV3 = TypingProxy.Nested.InitVar
2021

2122
@dataclass
2223
class CV:
23-
T_CV4 = TypingProxy.ClassVar
24-
cv0: TypingProxy.ClassVar[int] = 20
25-
cv1: TypingProxy.ClassVar = 30
24+
T_CV4 = TypingProxy.Nested.ClassVar
25+
cv0: TypingProxy.Nested.ClassVar[int] = 20
26+
cv1: TypingProxy.Nested.ClassVar = 30
2627
cv2: T_CV2
2728
cv3: T_CV3
2829
not_cv4: T_CV4 # When using string annotations, this field is not recognized as a ClassVar.
2930

3031
@dataclass
3132
class IV:
32-
T_IV4 = TypingProxy.InitVar
33-
iv0: TypingProxy.InitVar[int]
34-
iv1: TypingProxy.InitVar
33+
T_IV4 = TypingProxy.Nested.InitVar
34+
iv0: TypingProxy.Nested.InitVar[int]
35+
iv1: TypingProxy.Nested.InitVar
3536
iv2: T_IV2
3637
iv3: T_IV3
3738
not_iv4: T_IV4 # When using string annotations, this field is not recognized as an InitVar.

Lib/test/test_dataclasses/dataclass_module_4_str.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,30 @@
99
import typing
1010

1111
class TypingProxy:
12-
ClassVar = typing.ClassVar
13-
InitVar = dataclasses.InitVar
12+
class Nested:
13+
ClassVar = typing.ClassVar
14+
InitVar = dataclasses.InitVar
1415

15-
T_CV2 = TypingProxy.ClassVar[int]
16-
T_CV3 = TypingProxy.ClassVar
16+
T_CV2 = TypingProxy.Nested.ClassVar[int]
17+
T_CV3 = TypingProxy.Nested.ClassVar
1718

18-
T_IV2 = TypingProxy.InitVar[int]
19-
T_IV3 = TypingProxy.InitVar
19+
T_IV2 = TypingProxy.Nested.InitVar[int]
20+
T_IV3 = TypingProxy.Nested.InitVar
2021

2122
@dataclass
2223
class CV:
23-
T_CV4 = TypingProxy.ClassVar
24-
cv0: TypingProxy.ClassVar[int] = 20
25-
cv1: TypingProxy.ClassVar = 30
24+
T_CV4 = TypingProxy.Nested.ClassVar
25+
cv0: TypingProxy.Nested.ClassVar[int] = 20
26+
cv1: TypingProxy.Nested.ClassVar = 30
2627
cv2: T_CV2
2728
cv3: T_CV3
2829
not_cv4: T_CV4 # When using string annotations, this field is not recognized as a ClassVar.
2930

3031
@dataclass
3132
class IV:
32-
T_IV4 = TypingProxy.InitVar
33-
iv0: TypingProxy.InitVar[int]
34-
iv1: TypingProxy.InitVar
33+
T_IV4 = TypingProxy.Nested.InitVar
34+
iv0: TypingProxy.Nested.InitVar[int]
35+
iv1: TypingProxy.Nested.InitVar
3536
iv2: T_IV2
3637
iv3: T_IV3
3738
not_iv4: T_IV4 # When using string annotations, this field is not recognized as an InitVar.

0 commit comments

Comments
 (0)