Skip to content

Commit 03220fd

Browse files
authored
bpo-32427: Expose dataclasses.MISSING object. (#5045)
1 parent e325608 commit 03220fd

File tree

2 files changed

+70
-23
lines changed

2 files changed

+70
-23
lines changed

Lib/dataclasses.py

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
'field',
99
'FrozenInstanceError',
1010
'InitVar',
11+
'MISSING',
1112

1213
# Helper functions.
1314
'fields',
@@ -29,11 +30,11 @@ def __repr__(self):
2930
return '<factory>'
3031
_HAS_DEFAULT_FACTORY = _HAS_DEFAULT_FACTORY_CLASS()
3132

32-
# A sentinel object to detect if a parameter is supplied or not.
33-
class _MISSING_FACTORY:
34-
def __repr__(self):
35-
return '<missing>'
36-
_MISSING = _MISSING_FACTORY()
33+
# A sentinel object to detect if a parameter is supplied or not. Use
34+
# a class to give it a better repr.
35+
class _MISSING_TYPE:
36+
pass
37+
MISSING = _MISSING_TYPE()
3738

3839
# Since most per-field metadata will be unused, create an empty
3940
# read-only proxy that can be shared among all fields.
@@ -114,7 +115,7 @@ def __repr__(self):
114115
# This function is used instead of exposing Field creation directly,
115116
# so that a type checker can be told (via overloads) that this is a
116117
# function whose type depends on its parameters.
117-
def field(*, default=_MISSING, default_factory=_MISSING, init=True, repr=True,
118+
def field(*, default=MISSING, default_factory=MISSING, init=True, repr=True,
118119
hash=None, compare=True, metadata=None):
119120
"""Return an object to identify dataclass fields.
120121
@@ -130,7 +131,7 @@ def field(*, default=_MISSING, default_factory=_MISSING, init=True, repr=True,
130131
It is an error to specify both default and default_factory.
131132
"""
132133

133-
if default is not _MISSING and default_factory is not _MISSING:
134+
if default is not MISSING and default_factory is not MISSING:
134135
raise ValueError('cannot specify both default and default_factory')
135136
return Field(default, default_factory, init, repr, hash, compare,
136137
metadata)
@@ -149,12 +150,12 @@ def _tuple_str(obj_name, fields):
149150

150151

151152
def _create_fn(name, args, body, globals=None, locals=None,
152-
return_type=_MISSING):
153+
return_type=MISSING):
153154
# Note that we mutate locals when exec() is called. Caller beware!
154155
if locals is None:
155156
locals = {}
156157
return_annotation = ''
157-
if return_type is not _MISSING:
158+
if return_type is not MISSING:
158159
locals['_return_type'] = return_type
159160
return_annotation = '->_return_type'
160161
args = ','.join(args)
@@ -182,7 +183,7 @@ def _field_init(f, frozen, globals, self_name):
182183
# initialize this field.
183184

184185
default_name = f'_dflt_{f.name}'
185-
if f.default_factory is not _MISSING:
186+
if f.default_factory is not MISSING:
186187
if f.init:
187188
# This field has a default factory. If a parameter is
188189
# given, use it. If not, call the factory.
@@ -210,10 +211,10 @@ def _field_init(f, frozen, globals, self_name):
210211
else:
211212
# No default factory.
212213
if f.init:
213-
if f.default is _MISSING:
214+
if f.default is MISSING:
214215
# There's no default, just do an assignment.
215216
value = f.name
216-
elif f.default is not _MISSING:
217+
elif f.default is not MISSING:
217218
globals[default_name] = f.default
218219
value = f.name
219220
else:
@@ -236,14 +237,14 @@ def _init_param(f):
236237
# For example, the equivalent of 'x:int=3' (except instead of 'int',
237238
# reference a variable set to int, and instead of '3', reference a
238239
# variable set to 3).
239-
if f.default is _MISSING and f.default_factory is _MISSING:
240+
if f.default is MISSING and f.default_factory is MISSING:
240241
# There's no default, and no default_factory, just
241242
# output the variable name and type.
242243
default = ''
243-
elif f.default is not _MISSING:
244+
elif f.default is not MISSING:
244245
# There's a default, this will be the name that's used to look it up.
245246
default = f'=_dflt_{f.name}'
246-
elif f.default_factory is not _MISSING:
247+
elif f.default_factory is not MISSING:
247248
# There's a factory function. Set a marker.
248249
default = '=_HAS_DEFAULT_FACTORY'
249250
return f'{f.name}:_type_{f.name}{default}'
@@ -261,13 +262,13 @@ def _init_fn(fields, frozen, has_post_init, self_name):
261262
for f in fields:
262263
# Only consider fields in the __init__ call.
263264
if f.init:
264-
if not (f.default is _MISSING and f.default_factory is _MISSING):
265+
if not (f.default is MISSING and f.default_factory is MISSING):
265266
seen_default = True
266267
elif seen_default:
267268
raise TypeError(f'non-default argument {f.name!r} '
268269
'follows default argument')
269270

270-
globals = {'_MISSING': _MISSING,
271+
globals = {'MISSING': MISSING,
271272
'_HAS_DEFAULT_FACTORY': _HAS_DEFAULT_FACTORY}
272273

273274
body_lines = []
@@ -368,7 +369,7 @@ def _get_field(cls, a_name, a_type):
368369

369370
# If the default value isn't derived from field, then it's
370371
# only a normal default value. Convert it to a Field().
371-
default = getattr(cls, a_name, _MISSING)
372+
default = getattr(cls, a_name, MISSING)
372373
if isinstance(default, Field):
373374
f = default
374375
else:
@@ -404,7 +405,7 @@ def _get_field(cls, a_name, a_type):
404405

405406
# Special restrictions for ClassVar and InitVar.
406407
if f._field_type in (_FIELD_CLASSVAR, _FIELD_INITVAR):
407-
if f.default_factory is not _MISSING:
408+
if f.default_factory is not MISSING:
408409
raise TypeError(f'field {f.name} cannot have a '
409410
'default factory')
410411
# Should I check for other field settings? default_factory
@@ -474,7 +475,7 @@ def _process_class(cls, repr, eq, order, hash, init, frozen):
474475
# with the real default. This is so that normal class
475476
# introspection sees a real default value, not a Field.
476477
if isinstance(getattr(cls, f.name, None), Field):
477-
if f.default is _MISSING:
478+
if f.default is MISSING:
478479
# If there's no default, delete the class attribute.
479480
# This happens if we specify field(repr=False), for
480481
# example (that is, we specified a field object, but

Lib/test/test_dataclasses.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from dataclasses import (
22
dataclass, field, FrozenInstanceError, fields, asdict, astuple,
3-
make_dataclass, replace, InitVar, Field
3+
make_dataclass, replace, InitVar, Field, MISSING
44
)
55

66
import pickle
@@ -917,12 +917,12 @@ def validate_class(cls):
917917
param = next(params)
918918
self.assertEqual(param.name, 'k')
919919
self.assertIs (param.annotation, F)
920-
# Don't test for the default, since it's set to _MISSING
920+
# Don't test for the default, since it's set to MISSING
921921
self.assertEqual(param.kind, inspect.Parameter.POSITIONAL_OR_KEYWORD)
922922
param = next(params)
923923
self.assertEqual(param.name, 'l')
924924
self.assertIs (param.annotation, float)
925-
# Don't test for the default, since it's set to _MISSING
925+
# Don't test for the default, since it's set to MISSING
926926
self.assertEqual(param.kind, inspect.Parameter.POSITIONAL_OR_KEYWORD)
927927
self.assertRaises(StopIteration, next, params)
928928

@@ -948,6 +948,52 @@ class C:
948948

949949
validate_class(C)
950950

951+
def test_missing_default(self):
952+
# Test that MISSING works the same as a default not being
953+
# specified.
954+
@dataclass
955+
class C:
956+
x: int=field(default=MISSING)
957+
with self.assertRaisesRegex(TypeError,
958+
r'__init__\(\) missing 1 required '
959+
'positional argument'):
960+
C()
961+
self.assertNotIn('x', C.__dict__)
962+
963+
@dataclass
964+
class D:
965+
x: int
966+
with self.assertRaisesRegex(TypeError,
967+
r'__init__\(\) missing 1 required '
968+
'positional argument'):
969+
D()
970+
self.assertNotIn('x', D.__dict__)
971+
972+
def test_missing_default_factory(self):
973+
# Test that MISSING works the same as a default factory not
974+
# being specified (which is really the same as a default not
975+
# being specified, too).
976+
@dataclass
977+
class C:
978+
x: int=field(default_factory=MISSING)
979+
with self.assertRaisesRegex(TypeError,
980+
r'__init__\(\) missing 1 required '
981+
'positional argument'):
982+
C()
983+
self.assertNotIn('x', C.__dict__)
984+
985+
@dataclass
986+
class D:
987+
x: int=field(default=MISSING, default_factory=MISSING)
988+
with self.assertRaisesRegex(TypeError,
989+
r'__init__\(\) missing 1 required '
990+
'positional argument'):
991+
D()
992+
self.assertNotIn('x', D.__dict__)
993+
994+
def test_missing_repr(self):
995+
self.assertIn('MISSING_TYPE object', repr(MISSING))
996+
951997
def test_dont_include_other_annotations(self):
952998
@dataclass
953999
class C:

0 commit comments

Comments
 (0)