[Python-checkins] bpo-32505: dataclasses: raise TypeError if a member variable is of type Field, but doesn't have a type annotation. (GH-6192)

Eric V. Smith webhook-mailer at python.org
Thu Mar 22 16:28:52 EDT 2018


https://github.com/python/cpython/commit/56970b8ce9d23269d20a76f13c80e670c856ba7f
commit: 56970b8ce9d23269d20a76f13c80e670c856ba7f
branch: master
author: Eric V. Smith <ericvsmith at users.noreply.github.com>
committer: GitHub <noreply at github.com>
date: 2018-03-22T16:28:48-04:00
summary:

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.

files:
A Misc/NEWS.d/next/Library/2018-03-22-16-05-56.bpo-32505.YK1N8v.rst
M Lib/dataclasses.py
M Lib/test/test_dataclasses.py

diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py
index 41b5b5da325c..5d4d4a6100ca 100644
--- a/Lib/dataclasses.py
+++ b/Lib/dataclasses.py
@@ -573,22 +573,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.
@@ -663,10 +647,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
@@ -685,6 +684,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.
diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py
index 69ace36c2c58..8aff8ae140a5 100755
--- a/Lib/test/test_dataclasses.py
+++ b/Lib/test/test_dataclasses.py
@@ -24,6 +24,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:
@@ -1906,6 +1914,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
diff --git a/Misc/NEWS.d/next/Library/2018-03-22-16-05-56.bpo-32505.YK1N8v.rst b/Misc/NEWS.d/next/Library/2018-03-22-16-05-56.bpo-32505.YK1N8v.rst
new file mode 100644
index 000000000000..91e97bf53f65
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2018-03-22-16-05-56.bpo-32505.YK1N8v.rst
@@ -0,0 +1,2 @@
+Raise TypeError if a member variable of a dataclass is of type Field, but
+doesn't have a type annotation.



More information about the Python-checkins mailing list