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.
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
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.
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:
'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