Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 56970b8

Browse files
authored
bpo-32505: dataclasses: raise TypeError if a member variable is of type Field, but doesn't have a type annotation. (GH-6192)
If a dataclass has a member variable that's of type Field, but it doesn't have a type annotation, raise TypeError.
1 parent f757b72 commit 56970b8

3 files changed

Lines changed: 66 additions & 17 deletions

File tree

Lib/dataclasses.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -573,22 +573,6 @@ def _get_field(cls, a_name, a_type):
573573
return f
574574

575575

576-
def _find_fields(cls):
577-
# Return a list of Field objects, in order, for this class (and no
578-
# base classes). Fields are found from the class dict's
579-
# __annotations__ (which is guaranteed to be ordered). Default
580-
# values are from class attributes, if a field has a default. If
581-
# the default value is a Field(), then it contains additional
582-
# info beyond (and possibly including) the actual default value.
583-
# Pseudo-fields ClassVars and InitVars are included, despite the
584-
# fact that they're not real fields. That's dealt with later.
585-
586-
# If __annotations__ isn't present, then this class adds no new
587-
# annotations.
588-
annotations = cls.__dict__.get('__annotations__', {})
589-
return [_get_field(cls, name, type) for name, type in annotations.items()]
590-
591-
592576
def _set_new_attribute(cls, name, value):
593577
# Never overwrites an existing attribute. Returns True if the
594578
# attribute already exists.
@@ -663,10 +647,25 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
663647
if getattr(b, _PARAMS).frozen:
664648
any_frozen_base = True
665649

650+
# Annotations that are defined in this class (not in base
651+
# classes). If __annotations__ isn't present, then this class
652+
# adds no new annotations. We use this to compute fields that
653+
# are added by this class.
654+
# Fields are found from cls_annotations, which is guaranteed to be
655+
# ordered. Default values are from class attributes, if a field
656+
# has a default. If the default value is a Field(), then it
657+
# contains additional info beyond (and possibly including) the
658+
# actual default value. Pseudo-fields ClassVars and InitVars are
659+
# included, despite the fact that they're not real fields.
660+
# That's dealt with later.
661+
cls_annotations = cls.__dict__.get('__annotations__', {})
662+
666663
# Now find fields in our class. While doing so, validate some
667664
# things, and set the default values (as class attributes)
668665
# where we can.
669-
for f in _find_fields(cls):
666+
cls_fields = [_get_field(cls, name, type)
667+
for name, type in cls_annotations.items()]
668+
for f in cls_fields:
670669
fields[f.name] = f
671670

672671
# If the class attribute (which is the default value for
@@ -685,6 +684,11 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
685684
else:
686685
setattr(cls, f.name, f.default)
687686

687+
# Do we have any Field members that don't also have annotations?
688+
for name, value in cls.__dict__.items():
689+
if isinstance(value, Field) and not name in cls_annotations:
690+
raise TypeError(f'{name!r} is a field but has no type annotation')
691+
688692
# Check rules that apply if we are derived from any dataclasses.
689693
if has_dataclass_bases:
690694
# Raise an exception if any of our bases are frozen, but we're not.

Lib/test/test_dataclasses.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ class C:
2424
o = C()
2525
self.assertEqual(len(fields(C)), 0)
2626

27+
def test_no_fields_but_member_variable(self):
28+
@dataclass
29+
class C:
30+
i = 0
31+
32+
o = C()
33+
self.assertEqual(len(fields(C)), 0)
34+
2735
def test_one_field_no_default(self):
2836
@dataclass
2937
class C:
@@ -1906,6 +1914,41 @@ def test_helper_make_dataclass_no_types(self):
19061914
'z': 'typing.Any'})
19071915

19081916

1917+
class TestFieldNoAnnotation(unittest.TestCase):
1918+
def test_field_without_annotation(self):
1919+
with self.assertRaisesRegex(TypeError,
1920+
"'f' is a field but has no type annotation"):
1921+
@dataclass
1922+
class C:
1923+
f = field()
1924+
1925+
def test_field_without_annotation_but_annotation_in_base(self):
1926+
@dataclass
1927+
class B:
1928+
f: int
1929+
1930+
with self.assertRaisesRegex(TypeError,
1931+
"'f' is a field but has no type annotation"):
1932+
# This is still an error: make sure we don't pick up the
1933+
# type annotation in the base class.
1934+
@dataclass
1935+
class C(B):
1936+
f = field()
1937+
1938+
def test_field_without_annotation_but_annotation_in_base_not_dataclass(self):
1939+
# Same test, but with the base class not a dataclass.
1940+
class B:
1941+
f: int
1942+
1943+
with self.assertRaisesRegex(TypeError,
1944+
"'f' is a field but has no type annotation"):
1945+
# This is still an error: make sure we don't pick up the
1946+
# type annotation in the base class.
1947+
@dataclass
1948+
class C(B):
1949+
f = field()
1950+
1951+
19091952
class TestDocString(unittest.TestCase):
19101953
def assertDocStrEqual(self, a, b):
19111954
# Because 3.6 and 3.7 differ in how inspect.signature work
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Raise TypeError if a member variable of a dataclass is of type Field, but
2+
doesn't have a type annotation.

0 commit comments

Comments
 (0)