]> granicus.if.org Git - python/commitdiff
bpo-32427: Expose dataclasses.MISSING object. (#5045)
authorEric V. Smith <ericvsmith@users.noreply.github.com>
Fri, 29 Dec 2017 18:59:58 +0000 (13:59 -0500)
committerGitHub <noreply@github.com>
Fri, 29 Dec 2017 18:59:58 +0000 (13:59 -0500)
Lib/dataclasses.py
Lib/test/test_dataclasses.py

index 7a725dfb5208bbcb5647641187a9a04aea2f7fe7..eaaed63ef2826ec8b9b3b079980929bd811263bb 100644 (file)
@@ -8,6 +8,7 @@ __all__ = ['dataclass',
            'field',
            'FrozenInstanceError',
            'InitVar',
+           'MISSING',
 
            # Helper functions.
            'fields',
@@ -29,11 +30,11 @@ class _HAS_DEFAULT_FACTORY_CLASS:
         return '<factory>'
 _HAS_DEFAULT_FACTORY = _HAS_DEFAULT_FACTORY_CLASS()
 
-# A sentinel object to detect if a parameter is supplied or not.
-class _MISSING_FACTORY:
-    def __repr__(self):
-        return '<missing>'
-_MISSING = _MISSING_FACTORY()
+# A sentinel object to detect if a parameter is supplied or not.  Use
+#  a class to give it a better repr.
+class _MISSING_TYPE:
+    pass
+MISSING = _MISSING_TYPE()
 
 # Since most per-field metadata will be unused, create an empty
 #  read-only proxy that can be shared among all fields.
@@ -114,7 +115,7 @@ class Field:
 # This function is used instead of exposing Field creation directly,
 #  so that a type checker can be told (via overloads) that this is a
 #  function whose type depends on its parameters.
-def field(*, default=_MISSING, default_factory=_MISSING, init=True, repr=True,
+def field(*, default=MISSING, default_factory=MISSING, init=True, repr=True,
           hash=None, compare=True, metadata=None):
     """Return an object to identify dataclass fields.
 
@@ -130,7 +131,7 @@ def field(*, default=_MISSING, default_factory=_MISSING, init=True, repr=True,
     It is an error to specify both default and default_factory.
     """
 
-    if default is not _MISSING and default_factory is not _MISSING:
+    if default is not MISSING and default_factory is not MISSING:
         raise ValueError('cannot specify both default and default_factory')
     return Field(default, default_factory, init, repr, hash, compare,
                  metadata)
@@ -149,12 +150,12 @@ def _tuple_str(obj_name, fields):
 
 
 def _create_fn(name, args, body, globals=None, locals=None,
-               return_type=_MISSING):
+               return_type=MISSING):
     # Note that we mutate locals when exec() is called. Caller beware!
     if locals is None:
         locals = {}
     return_annotation = ''
-    if return_type is not _MISSING:
+    if return_type is not MISSING:
         locals['_return_type'] = return_type
         return_annotation = '->_return_type'
     args = ','.join(args)
@@ -182,7 +183,7 @@ def _field_init(f, frozen, globals, self_name):
     #  initialize this field.
 
     default_name = f'_dflt_{f.name}'
-    if f.default_factory is not _MISSING:
+    if f.default_factory is not MISSING:
         if f.init:
             # This field has a default factory.  If a parameter is
             #  given, use it.  If not, call the factory.
@@ -210,10 +211,10 @@ def _field_init(f, frozen, globals, self_name):
     else:
         # No default factory.
         if f.init:
-            if f.default is _MISSING:
+            if f.default is MISSING:
                 # There's no default, just do an assignment.
                 value = f.name
-            elif f.default is not _MISSING:
+            elif f.default is not MISSING:
                 globals[default_name] = f.default
                 value = f.name
         else:
@@ -236,14 +237,14 @@ def _init_param(f):
     #  For example, the equivalent of 'x:int=3' (except instead of 'int',
     #  reference a variable set to int, and instead of '3', reference a
     #  variable set to 3).
-    if f.default is _MISSING and f.default_factory is _MISSING:
+    if f.default is MISSING and f.default_factory is MISSING:
         # There's no default, and no default_factory, just
         #  output the variable name and type.
         default = ''
-    elif f.default is not _MISSING:
+    elif f.default is not MISSING:
         # There's a default, this will be the name that's used to look it up.
         default = f'=_dflt_{f.name}'
-    elif f.default_factory is not _MISSING:
+    elif f.default_factory is not MISSING:
         # There's a factory function. Set a marker.
         default = '=_HAS_DEFAULT_FACTORY'
     return f'{f.name}:_type_{f.name}{default}'
@@ -261,13 +262,13 @@ def _init_fn(fields, frozen, has_post_init, self_name):
     for f in fields:
         # Only consider fields in the __init__ call.
         if f.init:
-            if not (f.default is _MISSING and f.default_factory is _MISSING):
+            if not (f.default is MISSING and f.default_factory is MISSING):
                 seen_default = True
             elif seen_default:
                 raise TypeError(f'non-default argument {f.name!r} '
                                 'follows default argument')
 
-    globals = {'_MISSING': _MISSING,
+    globals = {'MISSING': MISSING,
                '_HAS_DEFAULT_FACTORY': _HAS_DEFAULT_FACTORY}
 
     body_lines = []
@@ -368,7 +369,7 @@ def _get_field(cls, a_name, a_type):
 
     # If the default value isn't derived from field, then it's
     #  only a normal default value.  Convert it to a Field().
-    default = getattr(cls, a_name, _MISSING)
+    default = getattr(cls, a_name, MISSING)
     if isinstance(default, Field):
         f = default
     else:
@@ -404,7 +405,7 @@ def _get_field(cls, a_name, a_type):
 
     # Special restrictions for ClassVar and InitVar.
     if f._field_type in (_FIELD_CLASSVAR, _FIELD_INITVAR):
-        if f.default_factory is not _MISSING:
+        if f.default_factory is not MISSING:
             raise TypeError(f'field {f.name} cannot have a '
                             'default factory')
         # Should I check for other field settings? default_factory
@@ -474,7 +475,7 @@ def _process_class(cls, repr, eq, order, hash, init, frozen):
         #  with the real default.  This is so that normal class
         #  introspection sees a real default value, not a Field.
         if isinstance(getattr(cls, f.name, None), Field):
-            if f.default is _MISSING:
+            if f.default is MISSING:
                 # If there's no default, delete the class attribute.
                 #  This happens if we specify field(repr=False), for
                 #  example (that is, we specified a field object, but
index 7fbea76ccd8ea6de627201a632a9c2cb0931a0fb..ed6956398827b47a3649eec6e6fd908893cebca5 100755 (executable)
@@ -1,6 +1,6 @@
 from dataclasses import (
     dataclass, field, FrozenInstanceError, fields, asdict, astuple,
-    make_dataclass, replace, InitVar, Field
+    make_dataclass, replace, InitVar, Field, MISSING
 )
 
 import pickle
@@ -917,12 +917,12 @@ class TestCase(unittest.TestCase):
             param = next(params)
             self.assertEqual(param.name, 'k')
             self.assertIs   (param.annotation, F)
-            # Don't test for the default, since it's set to _MISSING
+            # Don't test for the default, since it's set to MISSING
             self.assertEqual(param.kind, inspect.Parameter.POSITIONAL_OR_KEYWORD)
             param = next(params)
             self.assertEqual(param.name, 'l')
             self.assertIs   (param.annotation, float)
-            # Don't test for the default, since it's set to _MISSING
+            # Don't test for the default, since it's set to MISSING
             self.assertEqual(param.kind, inspect.Parameter.POSITIONAL_OR_KEYWORD)
             self.assertRaises(StopIteration, next, params)
 
@@ -948,6 +948,52 @@ class TestCase(unittest.TestCase):
 
         validate_class(C)
 
+    def test_missing_default(self):
+        # Test that MISSING works the same as a default not being
+        #  specified.
+        @dataclass
+        class C:
+            x: int=field(default=MISSING)
+        with self.assertRaisesRegex(TypeError,
+                                    r'__init__\(\) missing 1 required '
+                                    'positional argument'):
+            C()
+        self.assertNotIn('x', C.__dict__)
+
+        @dataclass
+        class D:
+            x: int
+        with self.assertRaisesRegex(TypeError,
+                                    r'__init__\(\) missing 1 required '
+                                    'positional argument'):
+            D()
+        self.assertNotIn('x', D.__dict__)
+
+    def test_missing_default_factory(self):
+        # Test that MISSING works the same as a default factory not
+        #  being specified (which is really the same as a default not
+        #  being specified, too).
+        @dataclass
+        class C:
+            x: int=field(default_factory=MISSING)
+        with self.assertRaisesRegex(TypeError,
+                                    r'__init__\(\) missing 1 required '
+                                    'positional argument'):
+            C()
+        self.assertNotIn('x', C.__dict__)
+
+        @dataclass
+        class D:
+            x: int=field(default=MISSING, default_factory=MISSING)
+        with self.assertRaisesRegex(TypeError,
+                                    r'__init__\(\) missing 1 required '
+                                    'positional argument'):
+            D()
+        self.assertNotIn('x', D.__dict__)
+
+    def test_missing_repr(self):
+        self.assertIn('MISSING_TYPE object', repr(MISSING))
+
     def test_dont_include_other_annotations(self):
         @dataclass
         class C: