Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 21 additions & 17 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,22 +572,6 @@ def _get_field(cls, a_name, a_type):
return f


def _find_fields(cls):
# Return a list of Field objects, in order, for this class (and no
# base classes). Fields are found from the class dict's
# __annotations__ (which is guaranteed to be ordered). Default
# values are from class attributes, if a field has a default. If
# the default value is a Field(), then it contains additional
# info beyond (and possibly including) the actual default value.
# Pseudo-fields ClassVars and InitVars are included, despite the
# fact that they're not real fields. That's dealt with later.

# If __annotations__ isn't present, then this class adds no new
# annotations.
annotations = cls.__dict__.get('__annotations__', {})
return [_get_field(cls, name, type) for name, type in annotations.items()]


def _set_new_attribute(cls, name, value):
# Never overwrites an existing attribute. Returns True if the
# attribute already exists.
Expand Down Expand Up @@ -662,10 +646,25 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
if getattr(b, _PARAMS).frozen:
any_frozen_base = True

# Annotations that are defined in this class (not in base
# classes). If __annotations__ isn't present, then this class
# adds no new annotations. We use this to compute fields that
# are added by this class.
# Fields are found from cls_annotations, which is guaranteed to be
# ordered. Default values are from class attributes, if a field
# has a default. If the default value is a Field(), then it
# contains additional info beyond (and possibly including) the
# actual default value. Pseudo-fields ClassVars and InitVars are
# included, despite the fact that they're not real fields.
# That's dealt with later.
cls_annotations = cls.__dict__.get('__annotations__', {})

# Now find fields in our class. While doing so, validate some
# things, and set the default values (as class attributes)
# where we can.
for f in _find_fields(cls):
cls_fields = [_get_field(cls, name, type)
for name, type in cls_annotations.items()]
for f in cls_fields:
fields[f.name] = f

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

# Do we have any Field members that don't also have annotations?
for name, value in cls.__dict__.items():
if isinstance(value, Field) and not name in cls_annotations:
raise TypeError(f'{name!r} is a field but has no type annotation')

# Check rules that apply if we are derived from any dataclasses.
if has_dataclass_bases:
# Raise an exception if any of our bases are frozen, but we're not.
Expand Down
43 changes: 43 additions & 0 deletions Lib/test/test_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ class C:
o = C()
self.assertEqual(len(fields(C)), 0)

def test_no_fields_but_member_variable(self):
@dataclass
class C:
i = 0

o = C()
self.assertEqual(len(fields(C)), 0)

def test_one_field_no_default(self):
@dataclass
class C:
Expand Down Expand Up @@ -1905,6 +1913,41 @@ def test_helper_make_dataclass_no_types(self):
'z': 'typing.Any'})


class TestFieldNoAnnotation(unittest.TestCase):
def test_field_without_annotation(self):
with self.assertRaisesRegex(TypeError,
"'f' is a field but has no type annotation"):
@dataclass
class C:
f = field()

def test_field_without_annotation_but_annotation_in_base(self):
@dataclass
class B:
f: int

with self.assertRaisesRegex(TypeError,
"'f' is a field but has no type annotation"):
# This is still an error: make sure we don't pick up the
# type annotation in the base class.
@dataclass
class C(B):
f = field()

def test_field_without_annotation_but_annotation_in_base_not_dataclass(self):
# Same test, but with the base class not a dataclass.
class B:
f: int

with self.assertRaisesRegex(TypeError,
"'f' is a field but has no type annotation"):
# This is still an error: make sure we don't pick up the
# type annotation in the base class.
@dataclass
class C(B):
f = field()


class TestDocString(unittest.TestCase):
def assertDocStrEqual(self, a, b):
# Because 3.6 and 3.7 differ in how inspect.signature work
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Raise TypeError if a member variable of a dataclass is of type Field, but
doesn't have a type annotation.