mirror of
https://github.com/python/cpython.git
synced 2024-11-24 02:15:30 +08:00
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.
This commit is contained in:
parent
f757b72b25
commit
56970b8ce9
@ -573,22 +573,6 @@ def _get_field(cls, a_name, a_type):
|
|||||||
return f
|
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):
|
def _set_new_attribute(cls, name, value):
|
||||||
# Never overwrites an existing attribute. Returns True if the
|
# Never overwrites an existing attribute. Returns True if the
|
||||||
# attribute already exists.
|
# attribute already exists.
|
||||||
@ -663,10 +647,25 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
|
|||||||
if getattr(b, _PARAMS).frozen:
|
if getattr(b, _PARAMS).frozen:
|
||||||
any_frozen_base = True
|
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
|
# Now find fields in our class. While doing so, validate some
|
||||||
# things, and set the default values (as class attributes)
|
# things, and set the default values (as class attributes)
|
||||||
# where we can.
|
# 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
|
fields[f.name] = f
|
||||||
|
|
||||||
# If the class attribute (which is the default value for
|
# 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):
|
|||||||
else:
|
else:
|
||||||
setattr(cls, f.name, f.default)
|
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.
|
# Check rules that apply if we are derived from any dataclasses.
|
||||||
if has_dataclass_bases:
|
if has_dataclass_bases:
|
||||||
# Raise an exception if any of our bases are frozen, but we're not.
|
# Raise an exception if any of our bases are frozen, but we're not.
|
||||||
|
@ -24,6 +24,14 @@ class TestCase(unittest.TestCase):
|
|||||||
o = C()
|
o = C()
|
||||||
self.assertEqual(len(fields(C)), 0)
|
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):
|
def test_one_field_no_default(self):
|
||||||
@dataclass
|
@dataclass
|
||||||
class C:
|
class C:
|
||||||
@ -1906,6 +1914,41 @@ class TestCase(unittest.TestCase):
|
|||||||
'z': 'typing.Any'})
|
'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):
|
class TestDocString(unittest.TestCase):
|
||||||
def assertDocStrEqual(self, a, b):
|
def assertDocStrEqual(self, a, b):
|
||||||
# Because 3.6 and 3.7 differ in how inspect.signature work
|
# Because 3.6 and 3.7 differ in how inspect.signature work
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
Raise TypeError if a member variable of a dataclass is of type Field, but
|
||||||
|
doesn't have a type annotation.
|
Loading…
Reference in New Issue
Block a user