Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 21 additions & 20 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
'field',
'FrozenInstanceError',
'InitVar',
'MISSING',

# Helper functions.
'fields',
Expand All @@ -29,11 +30,11 @@ def __repr__(self):
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.
Expand Down Expand Up @@ -114,7 +115,7 @@ def __repr__(self):
# 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.

Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand All @@ -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}'
Expand All @@ -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 = []
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
52 changes: 49 additions & 3 deletions Lib/test/test_dataclasses.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -917,12 +917,12 @@ def validate_class(cls):
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)

Expand All @@ -948,6 +948,52 @@ 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):
@dataclass
class C:
Expand Down