bpo-32427: Expose dataclasses.MISSING object. (#5045)
This commit is contained in:
parent
e325608740
commit
03220fdb26
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue