bpo-32427: Expose dataclasses.MISSING object. (#5045)

This commit is contained in:
Eric V. Smith 2017-12-29 13:59:58 -05:00 committed by GitHub
parent e325608740
commit 03220fdb26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 70 additions and 23 deletions

View File

@ -8,6 +8,7 @@ __all__ = ['dataclass',
'field', 'field',
'FrozenInstanceError', 'FrozenInstanceError',
'InitVar', 'InitVar',
'MISSING',
# Helper functions. # Helper functions.
'fields', 'fields',
@ -29,11 +30,11 @@ class _HAS_DEFAULT_FACTORY_CLASS:
return '<factory>' return '<factory>'
_HAS_DEFAULT_FACTORY = _HAS_DEFAULT_FACTORY_CLASS() _HAS_DEFAULT_FACTORY = _HAS_DEFAULT_FACTORY_CLASS()
# A sentinel object to detect if a parameter is supplied or not. # A sentinel object to detect if a parameter is supplied or not. Use
class _MISSING_FACTORY: # a class to give it a better repr.
def __repr__(self): class _MISSING_TYPE:
return '<missing>' pass
_MISSING = _MISSING_FACTORY() MISSING = _MISSING_TYPE()
# Since most per-field metadata will be unused, create an empty # Since most per-field metadata will be unused, create an empty
# read-only proxy that can be shared among all fields. # 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, # This function is used instead of exposing Field creation directly,
# so that a type checker can be told (via overloads) that this is a # so that a type checker can be told (via overloads) that this is a
# function whose type depends on its parameters. # 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): hash=None, compare=True, metadata=None):
"""Return an object to identify dataclass fields. """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. 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') raise ValueError('cannot specify both default and default_factory')
return Field(default, default_factory, init, repr, hash, compare, return Field(default, default_factory, init, repr, hash, compare,
metadata) metadata)
@ -149,12 +150,12 @@ def _tuple_str(obj_name, fields):
def _create_fn(name, args, body, globals=None, locals=None, 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! # Note that we mutate locals when exec() is called. Caller beware!
if locals is None: if locals is None:
locals = {} locals = {}
return_annotation = '' return_annotation = ''
if return_type is not _MISSING: if return_type is not MISSING:
locals['_return_type'] = return_type locals['_return_type'] = return_type
return_annotation = '->_return_type' return_annotation = '->_return_type'
args = ','.join(args) args = ','.join(args)
@ -182,7 +183,7 @@ def _field_init(f, frozen, globals, self_name):
# initialize this field. # initialize this field.
default_name = f'_dflt_{f.name}' default_name = f'_dflt_{f.name}'
if f.default_factory is not _MISSING: if f.default_factory is not MISSING:
if f.init: if f.init:
# This field has a default factory. If a parameter is # This field has a default factory. If a parameter is
# given, use it. If not, call the factory. # given, use it. If not, call the factory.
@ -210,10 +211,10 @@ def _field_init(f, frozen, globals, self_name):
else: else:
# No default factory. # No default factory.
if f.init: if f.init:
if f.default is _MISSING: if f.default is MISSING:
# There's no default, just do an assignment. # There's no default, just do an assignment.
value = f.name value = f.name
elif f.default is not _MISSING: elif f.default is not MISSING:
globals[default_name] = f.default globals[default_name] = f.default
value = f.name value = f.name
else: else:
@ -236,14 +237,14 @@ def _init_param(f):
# For example, the equivalent of 'x:int=3' (except instead of 'int', # For example, the equivalent of 'x:int=3' (except instead of 'int',
# reference a variable set to int, and instead of '3', reference a # reference a variable set to int, and instead of '3', reference a
# variable set to 3). # 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 # There's no default, and no default_factory, just
# output the variable name and type. # output the variable name and type.
default = '' 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. # There's a default, this will be the name that's used to look it up.
default = f'=_dflt_{f.name}' 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. # There's a factory function. Set a marker.
default = '=_HAS_DEFAULT_FACTORY' default = '=_HAS_DEFAULT_FACTORY'
return f'{f.name}:_type_{f.name}{default}' 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: for f in fields:
# Only consider fields in the __init__ call. # Only consider fields in the __init__ call.
if f.init: 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 seen_default = True
elif seen_default: elif seen_default:
raise TypeError(f'non-default argument {f.name!r} ' raise TypeError(f'non-default argument {f.name!r} '
'follows default argument') 'follows default argument')
globals = {'_MISSING': _MISSING, globals = {'MISSING': MISSING,
'_HAS_DEFAULT_FACTORY': _HAS_DEFAULT_FACTORY} '_HAS_DEFAULT_FACTORY': _HAS_DEFAULT_FACTORY}
body_lines = [] 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 # If the default value isn't derived from field, then it's
# only a normal default value. Convert it to a Field(). # 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): if isinstance(default, Field):
f = default f = default
else: else:
@ -404,7 +405,7 @@ def _get_field(cls, a_name, a_type):
# Special restrictions for ClassVar and InitVar. # Special restrictions for ClassVar and InitVar.
if f._field_type in (_FIELD_CLASSVAR, _FIELD_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 ' raise TypeError(f'field {f.name} cannot have a '
'default factory') 'default factory')
# Should I check for other field settings? 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 # with the real default. This is so that normal class
# introspection sees a real default value, not a Field. # introspection sees a real default value, not a Field.
if isinstance(getattr(cls, f.name, None), 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. # If there's no default, delete the class attribute.
# This happens if we specify field(repr=False), for # This happens if we specify field(repr=False), for
# example (that is, we specified a field object, but # example (that is, we specified a field object, but

View File

@ -1,6 +1,6 @@
from dataclasses import ( from dataclasses import (
dataclass, field, FrozenInstanceError, fields, asdict, astuple, dataclass, field, FrozenInstanceError, fields, asdict, astuple,
make_dataclass, replace, InitVar, Field make_dataclass, replace, InitVar, Field, MISSING
) )
import pickle import pickle
@ -917,12 +917,12 @@ class TestCase(unittest.TestCase):
param = next(params) param = next(params)
self.assertEqual(param.name, 'k') self.assertEqual(param.name, 'k')
self.assertIs (param.annotation, F) 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) self.assertEqual(param.kind, inspect.Parameter.POSITIONAL_OR_KEYWORD)
param = next(params) param = next(params)
self.assertEqual(param.name, 'l') self.assertEqual(param.name, 'l')
self.assertIs (param.annotation, float) 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.assertEqual(param.kind, inspect.Parameter.POSITIONAL_OR_KEYWORD)
self.assertRaises(StopIteration, next, params) self.assertRaises(StopIteration, next, params)
@ -948,6 +948,52 @@ class TestCase(unittest.TestCase):
validate_class(C) 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): def test_dont_include_other_annotations(self):
@dataclass @dataclass
class C: class C: