Skip to content

Commit 07390f2

Browse files
committed
fix: support nesting annotations when detecting ClassVar fields in dataclasses
1 parent 0c3c5ca commit 07390f2

4 files changed

Lines changed: 57 additions & 53 deletions

File tree

Lib/dataclasses.py

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -762,9 +762,8 @@ def _is_kw_only(a_type, dataclasses):
762762
return a_type is dataclasses.KW_ONLY
763763

764764

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

769768
# We can't perform a full type hint evaluation at the point where @dataclass
770769
# was invoked because class's module is not fully initialized yet. So we resort
@@ -773,9 +772,6 @@ def _is_type(annotation, cls, is_type_predicate, *is_type_predicate_args):
773772

774773
# - annotation is a string type annotation
775774
# - cls is the class that this annotation was found in
776-
# - is_type_predicate is a function called with (obj, *is_type_predicate_args)
777-
# that determines if obj is of the desired type.
778-
# - is_type_predicate_args is additional arguments forwarded to is_type_predicate
779775

780776
# Since this test does not do a local namespace lookup (and
781777
# instead only a module (global) lookup), there are some things it
@@ -806,25 +802,21 @@ def _is_type(annotation, cls, is_type_predicate, *is_type_predicate_args):
806802
# https://github.com/python/cpython/issues/77634 for details.
807803
global _MODULE_IDENTIFIER_RE
808804
if _MODULE_IDENTIFIER_RE is None:
809-
_MODULE_IDENTIFIER_RE = re.compile(r'(?:\s*(\w+)\s*\.)?\s*(\w+)')
805+
_MODULE_IDENTIFIER_RE = re.compile(r'^\s*(\w+(?:\s*\.\s*\w+)*)')
810806

811807
match = _MODULE_IDENTIFIER_RE.prefixmatch(annotation)
812808
if not match:
813-
return False
814-
815-
module_name = match.group(1)
816-
type_name = match.group(2)
809+
return None
817810

818-
if not module_name:
819-
# No module name, assume the class's module did
820-
# "from dataclasses import InitVar".
821-
ns = sys.modules.get(cls.__module__)
822-
else:
823-
# Look up module_name in the class's module.
824-
cls_module = sys.modules.get(cls.__module__)
825-
ns = cls_module.__dict__.get(module_name)
811+
# Note: _MODULE_IDENTIFIER_RE guarantees that path is non-empty
812+
path = match.group(1).split(".")
813+
root = sys.modules.get(cls.__module__)
814+
for path_item in path:
815+
root = getattr(root, path_item.strip(), None)
816+
if root is None:
817+
return None
826818

827-
return is_type_predicate(getattr(ns, type_name, None), *is_type_predicate_args)
819+
return root
828820

829821

830822
def _get_field(cls, a_name, a_type, default_kw_only):
@@ -862,16 +854,18 @@ def _get_field(cls, a_name, a_type, default_kw_only):
862854
# is actually of the correct type.
863855

864856
# For the complete discussion, see https://bugs.python.org/issue33453
857+
if isinstance(a_type, str):
858+
a_type_annotation = _get_type_from_annotation(a_type, cls)
859+
else:
860+
a_type_annotation = a_type
865861

866862
# If typing has not been imported, then it's impossible for any
867863
# annotation to be a ClassVar. So, only look for ClassVar if
868864
# typing has been imported by any module (not necessarily cls's
869865
# module).
870866
typing = sys.modules.get('typing')
871867
if typing:
872-
if (_is_classvar(a_type, typing)
873-
or (isinstance(f.type, str)
874-
and _is_type(f.type, cls, _is_classvar, typing))):
868+
if _is_classvar(a_type_annotation, typing):
875869
f._field_type = _FIELD_CLASSVAR
876870

877871
# If the type is InitVar, or if it's a matching string annotation,
@@ -880,9 +874,7 @@ def _get_field(cls, a_name, a_type, default_kw_only):
880874
# The module we're checking against is the module we're
881875
# currently in (dataclasses.py).
882876
dataclasses = sys.modules[__name__]
883-
if (_is_initvar(a_type, dataclasses)
884-
or (isinstance(f.type, str)
885-
and _is_type(f.type, cls, _is_initvar, dataclasses))):
877+
if _is_initvar(a_type_annotation, dataclasses):
886878
f._field_type = _FIELD_INITVAR
887879

888880
# Validations for individual fields. This is delayed until now,
@@ -1053,9 +1045,11 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
10531045
dataclasses = sys.modules[__name__]
10541046
for name, type in cls_annotations.items():
10551047
# See if this is a marker to change the value of kw_only.
1056-
if (_is_kw_only(type, dataclasses)
1057-
or (isinstance(type, str)
1058-
and _is_type(type, cls, _is_kw_only, dataclasses))):
1048+
if isinstance(type, str):
1049+
a_type_annotation = _get_type_from_annotation(type, cls)
1050+
else:
1051+
a_type_annotation = type
1052+
if _is_kw_only(a_type_annotation, dataclasses):
10591053
# Switch the default to kw_only=True, and ignore this
10601054
# annotation: it's not a real field.
10611055
if KW_ONLY_seen:

Lib/test/test_dataclasses/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4643,6 +4643,14 @@ def custom_dataclass(cls, *args, **kwargs):
46434643
self.assertEqual(c.x, 10)
46444644
self.assertEqual(c.__custom__, True)
46454645

4646+
def test_empty_annotation_string(self):
4647+
@dataclass
4648+
class DataclassWithEmptyTypeAnnotation:
4649+
x: ""
4650+
4651+
c = DataclassWithEmptyTypeAnnotation(10)
4652+
self.assertEqual(c.x, 10)
4653+
46464654

46474655
class TestReplace(unittest.TestCase):
46484656
def test(self):

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)