mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
Update dataclasses from CPython 3.10.6
This commit is contained in:
1 parent
945bbe9bf6
commit
330981c533
7 files changed
+1013
-80
No files matched your search
Vendored
+222
-67
@@ -6,8 +6,9 @@ import inspect
|
||||
import keyword
|
||||
import builtins
|
||||
import functools
|
||||
import abc
|
||||
import _thread
|
||||
from types import GenericAlias
|
||||
from types import FunctionType, GenericAlias
|
||||
|
||||
|
||||
__all__ = ['dataclass',
|
||||
@@ -15,6 +16,7 @@ __all__ = ['dataclass',
|
||||
'Field',
|
||||
'FrozenInstanceError',
|
||||
'InitVar',
|
||||
'KW_ONLY',
|
||||
'MISSING',
|
||||
|
||||
# Helper functions.
|
||||
@@ -151,6 +153,20 @@ __all__ = ['dataclass',
|
||||
#
|
||||
# See _hash_action (below) for a coded version of this table.
|
||||
|
||||
# __match_args__
|
||||
#
|
||||
# +--- match_args= parameter
|
||||
# |
|
||||
# v | | |
|
||||
# | no | yes | <--- class has __match_args__ in __dict__?
|
||||
# +=======+=======+=======+
|
||||
# | False | | |
|
||||
# +-------+-------+-------+
|
||||
# | True | add | | <- the default
|
||||
# +=======+=======+=======+
|
||||
# __match_args__ is always added unless the class already defines it. It is a
|
||||
# tuple of __init__ parameter names; non-init fields must be matched by keyword.
|
||||
|
||||
|
||||
# Raised when an attempt is made to modify a frozen class.
|
||||
class FrozenInstanceError(AttributeError): pass
|
||||
@@ -169,6 +185,12 @@ class _MISSING_TYPE:
|
||||
pass
|
||||
MISSING = _MISSING_TYPE()
|
||||
|
||||
# A sentinel object to indicate that following fields are keyword-only by
|
||||
# default. Use a class to give it a better repr.
|
||||
class _KW_ONLY_TYPE:
|
||||
pass
|
||||
KW_ONLY = _KW_ONLY_TYPE()
|
||||
|
||||
# Since most per-field metadata will be unused, create an empty
|
||||
# read-only proxy that can be shared among all fields.
|
||||
_EMPTY_METADATA = types.MappingProxyType({})
|
||||
@@ -200,18 +222,14 @@ _POST_INIT_NAME = '__post_init__'
|
||||
# https://bugs.python.org/issue33453 for details.
|
||||
_MODULE_IDENTIFIER_RE = re.compile(r'^(?:\s*(\w+)\s*\.)?\s*(\w+)')
|
||||
|
||||
class _InitVarMeta(type):
|
||||
def __getitem__(self, params):
|
||||
return InitVar(params)
|
||||
|
||||
class InitVar(metaclass=_InitVarMeta):
|
||||
class InitVar:
|
||||
__slots__ = ('type', )
|
||||
|
||||
def __init__(self, type):
|
||||
self.type = type
|
||||
|
||||
def __repr__(self):
|
||||
if isinstance(self.type, type):
|
||||
if isinstance(self.type, type) and not isinstance(self.type, GenericAlias):
|
||||
type_name = self.type.__name__
|
||||
else:
|
||||
# typing objects, e.g. List[int]
|
||||
@@ -241,11 +259,12 @@ class Field:
|
||||
'init',
|
||||
'compare',
|
||||
'metadata',
|
||||
'kw_only',
|
||||
'_field_type', # Private: not to be used by user code.
|
||||
)
|
||||
|
||||
def __init__(self, default, default_factory, init, repr, hash, compare,
|
||||
metadata):
|
||||
metadata, kw_only):
|
||||
self.name = None
|
||||
self.type = None
|
||||
self.default = default
|
||||
@@ -257,6 +276,7 @@ class Field:
|
||||
self.metadata = (_EMPTY_METADATA
|
||||
if metadata is None else
|
||||
types.MappingProxyType(metadata))
|
||||
self.kw_only = kw_only
|
||||
self._field_type = None
|
||||
|
||||
def __repr__(self):
|
||||
@@ -270,6 +290,7 @@ class Field:
|
||||
f'hash={self.hash!r},'
|
||||
f'compare={self.compare!r},'
|
||||
f'metadata={self.metadata!r},'
|
||||
f'kw_only={self.kw_only!r},'
|
||||
f'_field_type={self._field_type}'
|
||||
')')
|
||||
|
||||
@@ -323,17 +344,19 @@ class _DataclassParams:
|
||||
# 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,
|
||||
hash=None, compare=True, metadata=None):
|
||||
hash=None, compare=True, metadata=None, kw_only=MISSING):
|
||||
"""Return an object to identify dataclass fields.
|
||||
|
||||
default is the default value of the field. default_factory is a
|
||||
0-argument function called to initialize a field's value. If init
|
||||
is True, the field will be a parameter to the class's __init__()
|
||||
function. If repr is True, the field will be included in the
|
||||
object's repr(). If hash is True, the field will be included in
|
||||
the object's hash(). If compare is True, the field will be used
|
||||
in comparison functions. metadata, if specified, must be a
|
||||
mapping which is stored but not otherwise examined by dataclass.
|
||||
is true, the field will be a parameter to the class's __init__()
|
||||
function. If repr is true, the field will be included in the
|
||||
object's repr(). If hash is true, the field will be included in the
|
||||
object's hash(). If compare is true, the field will be used in
|
||||
comparison functions. metadata, if specified, must be a mapping
|
||||
which is stored but not otherwise examined by dataclass. If kw_only
|
||||
is true, the field will become a keyword-only parameter to
|
||||
__init__().
|
||||
|
||||
It is an error to specify both default and default_factory.
|
||||
"""
|
||||
@@ -341,7 +364,16 @@ def field(*, default=MISSING, default_factory=MISSING, init=True, repr=True,
|
||||
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)
|
||||
metadata, kw_only)
|
||||
|
||||
|
||||
def _fields_in_init_order(fields):
|
||||
# Returns the fields as __init__ will output them. It returns 2 tuples:
|
||||
# the first for normal args, and the second for keyword args.
|
||||
|
||||
return (tuple(f for f in fields if f.init and not f.kw_only),
|
||||
tuple(f for f in fields if f.init and f.kw_only)
|
||||
)
|
||||
|
||||
|
||||
def _tuple_str(obj_name, fields):
|
||||
@@ -398,7 +430,6 @@ def _create_fn(name, args, body, *, globals=None, locals=None,
|
||||
|
||||
local_vars = ', '.join(locals.keys())
|
||||
txt = f"def __create_fn__({local_vars}):\n{txt}\n return {name}"
|
||||
|
||||
ns = {}
|
||||
exec(txt, globals, ns)
|
||||
return ns['__create_fn__'](**locals)
|
||||
@@ -416,7 +447,7 @@ def _field_assign(frozen, name, value, self_name):
|
||||
return f'{self_name}.{name}={value}'
|
||||
|
||||
|
||||
def _field_init(f, frozen, globals, self_name):
|
||||
def _field_init(f, frozen, globals, self_name, slots):
|
||||
# Return the text of the line in the body of __init__ that will
|
||||
# initialize this field.
|
||||
|
||||
@@ -456,9 +487,15 @@ def _field_init(f, frozen, globals, self_name):
|
||||
globals[default_name] = f.default
|
||||
value = f.name
|
||||
else:
|
||||
# This field does not need initialization. Signify that
|
||||
# to the caller by returning None.
|
||||
return None
|
||||
# If the class has slots, then initialize this field.
|
||||
if slots and f.default is not MISSING:
|
||||
globals[default_name] = f.default
|
||||
value = default_name
|
||||
else:
|
||||
# This field does not need initialization: reading from it will
|
||||
# just use the class attribute that contains the default.
|
||||
# Signify that to the caller by returning None.
|
||||
return None
|
||||
|
||||
# Only test this now, so that we can create variables for the
|
||||
# default. However, return None to signify that we're not going
|
||||
@@ -489,7 +526,8 @@ def _init_param(f):
|
||||
return f'{f.name}:_type_{f.name}{default}'
|
||||
|
||||
|
||||
def _init_fn(fields, frozen, has_post_init, self_name, globals):
|
||||
def _init_fn(fields, std_fields, kw_only_fields, frozen, has_post_init,
|
||||
self_name, globals, slots):
|
||||
# fields contains both real fields and InitVar pseudo-fields.
|
||||
|
||||
# Make sure we don't have fields without defaults following fields
|
||||
@@ -497,9 +535,10 @@ def _init_fn(fields, frozen, has_post_init, self_name, globals):
|
||||
# function source code, but catching it here gives a better error
|
||||
# message, and future-proofs us in case we build up the function
|
||||
# using ast.
|
||||
|
||||
seen_default = False
|
||||
for f in fields:
|
||||
# Only consider fields in the __init__ call.
|
||||
for f in std_fields:
|
||||
# Only consider the non-kw-only fields in the __init__ call.
|
||||
if f.init:
|
||||
if not (f.default is MISSING and f.default_factory is MISSING):
|
||||
seen_default = True
|
||||
@@ -515,7 +554,7 @@ def _init_fn(fields, frozen, has_post_init, self_name, globals):
|
||||
|
||||
body_lines = []
|
||||
for f in fields:
|
||||
line = _field_init(f, frozen, locals, self_name)
|
||||
line = _field_init(f, frozen, locals, self_name, slots)
|
||||
# line is None means that this field doesn't require
|
||||
# initialization (it's a pseudo-field). Just skip it.
|
||||
if line:
|
||||
@@ -531,8 +570,15 @@ def _init_fn(fields, frozen, has_post_init, self_name, globals):
|
||||
if not body_lines:
|
||||
body_lines = ['pass']
|
||||
|
||||
_init_params = [_init_param(f) for f in std_fields]
|
||||
if kw_only_fields:
|
||||
# Add the keyword-only args. Because the * can only be added if
|
||||
# there's at least one keyword-only arg, there needs to be a test here
|
||||
# (instead of just concatenting the lists together).
|
||||
_init_params += ['*']
|
||||
_init_params += [_init_param(f) for f in kw_only_fields]
|
||||
return _create_fn('__init__',
|
||||
[self_name] + [_init_param(f) for f in fields if f.init],
|
||||
[self_name] + _init_params,
|
||||
body_lines,
|
||||
locals=locals,
|
||||
globals=globals,
|
||||
@@ -611,6 +657,9 @@ def _is_initvar(a_type, dataclasses):
|
||||
return (a_type is dataclasses.InitVar
|
||||
or type(a_type) is dataclasses.InitVar)
|
||||
|
||||
def _is_kw_only(a_type, dataclasses):
|
||||
return a_type is dataclasses.KW_ONLY
|
||||
|
||||
|
||||
def _is_type(annotation, cls, a_module, a_type, is_type_predicate):
|
||||
# Given a type annotation string, does it refer to a_type in
|
||||
@@ -671,10 +720,11 @@ def _is_type(annotation, cls, a_module, a_type, is_type_predicate):
|
||||
return False
|
||||
|
||||
|
||||
def _get_field(cls, a_name, a_type):
|
||||
# Return a Field object for this field name and type. ClassVars
|
||||
# and InitVars are also returned, but marked as such (see
|
||||
# f._field_type).
|
||||
def _get_field(cls, a_name, a_type, default_kw_only):
|
||||
# Return a Field object for this field name and type. ClassVars and
|
||||
# InitVars are also returned, but marked as such (see f._field_type).
|
||||
# default_kw_only is the value of kw_only to use if there isn't a field()
|
||||
# that defines it.
|
||||
|
||||
# If the default value isn't derived from Field, then it's only a
|
||||
# normal default value. Convert it to a Field().
|
||||
@@ -745,6 +795,19 @@ def _get_field(cls, a_name, a_type):
|
||||
# init=<not-the-default-init-value>)? It makes no sense for
|
||||
# ClassVar and InitVar to specify init=<anything>.
|
||||
|
||||
# kw_only validation and assignment.
|
||||
if f._field_type in (_FIELD, _FIELD_INITVAR):
|
||||
# For real and InitVar fields, if kw_only wasn't specified use the
|
||||
# default value.
|
||||
if f.kw_only is MISSING:
|
||||
f.kw_only = default_kw_only
|
||||
else:
|
||||
# Make sure kw_only isn't set for ClassVars
|
||||
assert f._field_type is _FIELD_CLASSVAR
|
||||
if f.kw_only is not MISSING:
|
||||
raise TypeError(f'field {f.name} is a ClassVar but specifies '
|
||||
'kw_only')
|
||||
|
||||
# For real fields, disallow mutable defaults for known types.
|
||||
if f._field_type is _FIELD and isinstance(f.default, (list, dict, set)):
|
||||
raise ValueError(f'mutable default {type(f.default)} for field '
|
||||
@@ -752,12 +815,19 @@ def _get_field(cls, a_name, a_type):
|
||||
|
||||
return f
|
||||
|
||||
def _set_qualname(cls, value):
|
||||
# Ensure that the functions returned from _create_fn uses the proper
|
||||
# __qualname__ (the class they belong to).
|
||||
if isinstance(value, FunctionType):
|
||||
value.__qualname__ = f"{cls.__qualname__}.{value.__name__}"
|
||||
return value
|
||||
|
||||
def _set_new_attribute(cls, name, value):
|
||||
# Never overwrites an existing attribute. Returns True if the
|
||||
# attribute already exists.
|
||||
if name in cls.__dict__:
|
||||
return True
|
||||
_set_qualname(cls, value)
|
||||
setattr(cls, name, value)
|
||||
return False
|
||||
|
||||
@@ -772,7 +842,7 @@ def _hash_set_none(cls, fields, globals):
|
||||
|
||||
def _hash_add(cls, fields, globals):
|
||||
flds = [f for f in fields if (f.compare if f.hash is None else f.hash)]
|
||||
return _hash_fn(flds, globals)
|
||||
return _set_qualname(cls, _hash_fn(flds, globals))
|
||||
|
||||
def _hash_exception(cls, fields, globals):
|
||||
# Raise an exception.
|
||||
@@ -809,7 +879,8 @@ _hash_action = {(False, False, False, False): None,
|
||||
# version of this table.
|
||||
|
||||
|
||||
def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
|
||||
def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
|
||||
match_args, kw_only, slots):
|
||||
# Now that dicts retain insertion order, there's no reason to use
|
||||
# an ordered dict. I am leveraging that ordering here, because
|
||||
# derived class fields overwrite base class fields, but the order
|
||||
@@ -863,8 +934,27 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
|
||||
# Now find fields in our class. While doing so, validate some
|
||||
# things, and set the default values (as class attributes) where
|
||||
# we can.
|
||||
cls_fields = [_get_field(cls, name, type)
|
||||
for name, type in cls_annotations.items()]
|
||||
cls_fields = []
|
||||
# Get a reference to this module for the _is_kw_only() test.
|
||||
KW_ONLY_seen = False
|
||||
dataclasses = sys.modules[__name__]
|
||||
for name, type in cls_annotations.items():
|
||||
# See if this is a marker to change the value of kw_only.
|
||||
if (_is_kw_only(type, dataclasses)
|
||||
or (isinstance(type, str)
|
||||
and _is_type(type, cls, dataclasses, dataclasses.KW_ONLY,
|
||||
_is_kw_only))):
|
||||
# Switch the default to kw_only=True, and ignore this
|
||||
# annotation: it's not a real field.
|
||||
if KW_ONLY_seen:
|
||||
raise TypeError(f'{name!r} is KW_ONLY, but KW_ONLY '
|
||||
'has already been specified')
|
||||
KW_ONLY_seen = True
|
||||
kw_only = True
|
||||
else:
|
||||
# Otherwise it's a field of some type.
|
||||
cls_fields.append(_get_field(cls, name, type, kw_only))
|
||||
|
||||
for f in cls_fields:
|
||||
fields[f.name] = f
|
||||
|
||||
@@ -919,15 +1009,22 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
|
||||
if order and not eq:
|
||||
raise ValueError('eq must be true if order is true')
|
||||
|
||||
# Include InitVars and regular fields (so, not ClassVars). This is
|
||||
# initialized here, outside of the "if init:" test, because std_init_fields
|
||||
# is used with match_args, below.
|
||||
all_init_fields = [f for f in fields.values()
|
||||
if f._field_type in (_FIELD, _FIELD_INITVAR)]
|
||||
(std_init_fields,
|
||||
kw_only_init_fields) = _fields_in_init_order(all_init_fields)
|
||||
|
||||
if init:
|
||||
# Does this class have a post-init function?
|
||||
has_post_init = hasattr(cls, _POST_INIT_NAME)
|
||||
|
||||
# Include InitVars and regular fields (so, not ClassVars).
|
||||
flds = [f for f in fields.values()
|
||||
if f._field_type in (_FIELD, _FIELD_INITVAR)]
|
||||
_set_new_attribute(cls, '__init__',
|
||||
_init_fn(flds,
|
||||
_init_fn(all_init_fields,
|
||||
std_init_fields,
|
||||
kw_only_init_fields,
|
||||
frozen,
|
||||
has_post_init,
|
||||
# The name to use for the "self"
|
||||
@@ -936,6 +1033,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
|
||||
'__dataclass_self__' if 'self' in fields
|
||||
else 'self',
|
||||
globals,
|
||||
slots,
|
||||
))
|
||||
|
||||
# Get the fields as a list, and include only real fields. This is
|
||||
@@ -947,7 +1045,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
|
||||
_set_new_attribute(cls, '__repr__', _repr_fn(flds, globals))
|
||||
|
||||
if eq:
|
||||
# Create _eq__ method. There's no need for a __ne__ method,
|
||||
# Create __eq__ method. There's no need for a __ne__ method,
|
||||
# since python will call __eq__ and negate it.
|
||||
flds = [f for f in field_list if f.compare]
|
||||
self_tuple = _tuple_str('self', flds)
|
||||
@@ -995,11 +1093,70 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen):
|
||||
cls.__doc__ = (cls.__name__ +
|
||||
str(inspect.signature(cls)).replace(' -> None', ''))
|
||||
|
||||
if match_args:
|
||||
# I could probably compute this once
|
||||
_set_new_attribute(cls, '__match_args__',
|
||||
tuple(f.name for f in std_init_fields))
|
||||
|
||||
if slots:
|
||||
cls = _add_slots(cls, frozen)
|
||||
|
||||
abc.update_abstractmethods(cls)
|
||||
|
||||
return cls
|
||||
|
||||
|
||||
# _dataclass_getstate and _dataclass_setstate are needed for pickling frozen
|
||||
# classes with slots. These could be slighly more performant if we generated
|
||||
# the code instead of iterating over fields. But that can be a project for
|
||||
# another day, if performance becomes an issue.
|
||||
def _dataclass_getstate(self):
|
||||
return [getattr(self, f.name) for f in fields(self)]
|
||||
|
||||
|
||||
def _dataclass_setstate(self, state):
|
||||
for field, value in zip(fields(self), state):
|
||||
# use setattr because dataclass may be frozen
|
||||
object.__setattr__(self, field.name, value)
|
||||
|
||||
|
||||
def _add_slots(cls, is_frozen):
|
||||
# Need to create a new class, since we can't set __slots__
|
||||
# after a class has been created.
|
||||
|
||||
# Make sure __slots__ isn't already set.
|
||||
if '__slots__' in cls.__dict__:
|
||||
raise TypeError(f'{cls.__name__} already specifies __slots__')
|
||||
|
||||
# Create a new dict for our new class.
|
||||
cls_dict = dict(cls.__dict__)
|
||||
field_names = tuple(f.name for f in fields(cls))
|
||||
cls_dict['__slots__'] = field_names
|
||||
for field_name in field_names:
|
||||
# Remove our attributes, if present. They'll still be
|
||||
# available in _MARKER.
|
||||
cls_dict.pop(field_name, None)
|
||||
|
||||
# Remove __dict__ itself.
|
||||
cls_dict.pop('__dict__', None)
|
||||
|
||||
# And finally create the class.
|
||||
qualname = getattr(cls, '__qualname__', None)
|
||||
cls = type(cls)(cls.__name__, cls.__bases__, cls_dict)
|
||||
if qualname is not None:
|
||||
cls.__qualname__ = qualname
|
||||
|
||||
if is_frozen:
|
||||
# Need this for pickling frozen classes with slots.
|
||||
cls.__getstate__ = _dataclass_getstate
|
||||
cls.__setstate__ = _dataclass_setstate
|
||||
|
||||
return cls
|
||||
|
||||
|
||||
def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,
|
||||
unsafe_hash=False, frozen=False):
|
||||
unsafe_hash=False, frozen=False, match_args=True,
|
||||
kw_only=False, slots=False):
|
||||
"""Returns the same class as was passed in, with dunder methods
|
||||
added based on the fields defined in the class.
|
||||
|
||||
@@ -1009,11 +1166,15 @@ def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,
|
||||
repr is true, a __repr__() method is added. If order is true, rich
|
||||
comparison dunder methods are added. If unsafe_hash is true, a
|
||||
__hash__() method function is added. If frozen is true, fields may
|
||||
not be assigned to after instance creation.
|
||||
not be assigned to after instance creation. If match_args is true,
|
||||
the __match_args__ tuple is added. If kw_only is true, then by
|
||||
default all fields are keyword-only. If slots is true, an
|
||||
__slots__ attribute is added.
|
||||
"""
|
||||
|
||||
def wrap(cls):
|
||||
return _process_class(cls, init, repr, eq, order, unsafe_hash, frozen)
|
||||
return _process_class(cls, init, repr, eq, order, unsafe_hash,
|
||||
frozen, match_args, kw_only, slots)
|
||||
|
||||
# See if we're being called as @dataclass or @dataclass().
|
||||
if cls is None:
|
||||
@@ -1050,7 +1211,7 @@ def _is_dataclass_instance(obj):
|
||||
def is_dataclass(obj):
|
||||
"""Returns True if obj is a dataclass or an instance of a
|
||||
dataclass."""
|
||||
cls = obj if isinstance(obj, type) else type(obj)
|
||||
cls = obj if isinstance(obj, type) and not isinstance(obj, GenericAlias) else type(obj)
|
||||
return hasattr(cls, _FIELDS)
|
||||
|
||||
|
||||
@@ -1172,7 +1333,7 @@ def _astuple_inner(obj, tuple_factory):
|
||||
|
||||
def make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True,
|
||||
repr=True, eq=True, order=False, unsafe_hash=False,
|
||||
frozen=False):
|
||||
frozen=False, match_args=True, kw_only=False, slots=False):
|
||||
"""Return a new dynamically created dataclass.
|
||||
|
||||
The dataclass name will be 'cls_name'. 'fields' is an iterable
|
||||
@@ -1198,14 +1359,12 @@ def make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True,
|
||||
|
||||
if namespace is None:
|
||||
namespace = {}
|
||||
else:
|
||||
# Copy namespace since we're going to mutate it.
|
||||
namespace = namespace.copy()
|
||||
|
||||
# While we're looking through the field names, validate that they
|
||||
# are identifiers, are not keywords, and not duplicates.
|
||||
seen = set()
|
||||
anns = {}
|
||||
annotations = {}
|
||||
defaults = {}
|
||||
for item in fields:
|
||||
if isinstance(item, str):
|
||||
name = item
|
||||
@@ -1214,7 +1373,7 @@ def make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True,
|
||||
name, tp, = item
|
||||
elif len(item) == 3:
|
||||
name, tp, spec = item
|
||||
namespace[name] = spec
|
||||
defaults[name] = spec
|
||||
else:
|
||||
raise TypeError(f'Invalid field: {item!r}')
|
||||
|
||||
@@ -1226,17 +1385,25 @@ def make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True,
|
||||
raise TypeError(f'Field name duplicated: {name!r}')
|
||||
|
||||
seen.add(name)
|
||||
anns[name] = tp
|
||||
annotations[name] = tp
|
||||
|
||||
# Update 'ns' with the user-supplied namespace plus our calculated values.
|
||||
def exec_body_callback(ns):
|
||||
ns.update(namespace)
|
||||
ns.update(defaults)
|
||||
ns['__annotations__'] = annotations
|
||||
|
||||
namespace['__annotations__'] = anns
|
||||
# We use `types.new_class()` instead of simply `type()` to allow dynamic creation
|
||||
# of generic dataclassses.
|
||||
cls = types.new_class(cls_name, bases, {}, lambda ns: ns.update(namespace))
|
||||
# of generic dataclasses.
|
||||
cls = types.new_class(cls_name, bases, {}, exec_body_callback)
|
||||
|
||||
# Apply the normal decorator.
|
||||
return dataclass(cls, init=init, repr=repr, eq=eq, order=order,
|
||||
unsafe_hash=unsafe_hash, frozen=frozen)
|
||||
unsafe_hash=unsafe_hash, frozen=frozen,
|
||||
match_args=match_args, kw_only=kw_only, slots=slots)
|
||||
|
||||
|
||||
def replace(*args, **changes):
|
||||
def replace(obj, /, **changes):
|
||||
"""Return a new object replacing specified fields with new values.
|
||||
|
||||
This is especially useful for frozen classes. Example usage:
|
||||
@@ -1250,17 +1417,6 @@ def replace(*args, **changes):
|
||||
c1 = replace(c, x=3)
|
||||
assert c1.x == 3 and c1.y == 2
|
||||
"""
|
||||
if len(args) > 1:
|
||||
raise TypeError(f'replace() takes 1 positional argument but {len(args)} were given')
|
||||
if args:
|
||||
obj, = args
|
||||
elif 'obj' in changes:
|
||||
obj = changes.pop('obj')
|
||||
import warnings
|
||||
warnings.warn("Passing 'obj' as keyword argument is deprecated",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
else:
|
||||
raise TypeError("replace() missing 1 required positional argument: 'obj'")
|
||||
|
||||
# We're going to mutate 'changes', but that's okay because it's a
|
||||
# new dict, even if called with 'replace(obj, **my_changes)'.
|
||||
@@ -1296,4 +1452,3 @@ def replace(*args, **changes):
|
||||
# changes that aren't fields, this will correctly raise a
|
||||
# TypeError.
|
||||
return obj.__class__(**changes)
|
||||
replace.__text_signature__ = '(obj, /, **kwargs)'
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
#from __future__ import annotations
|
||||
USING_STRINGS = False
|
||||
|
||||
# dataclass_module_1.py and dataclass_module_1_str.py are identical
|
||||
# except only the latter uses string annotations.
|
||||
|
||||
import dataclasses
|
||||
import typing
|
||||
|
||||
T_CV2 = typing.ClassVar[int]
|
||||
T_CV3 = typing.ClassVar
|
||||
|
||||
T_IV2 = dataclasses.InitVar[int]
|
||||
T_IV3 = dataclasses.InitVar
|
||||
|
||||
@dataclasses.dataclass
|
||||
class CV:
|
||||
T_CV4 = typing.ClassVar
|
||||
cv0: typing.ClassVar[int] = 20
|
||||
cv1: typing.ClassVar = 30
|
||||
cv2: T_CV2
|
||||
cv3: T_CV3
|
||||
not_cv4: T_CV4 # When using string annotations, this field is not recognized as a ClassVar.
|
||||
|
||||
@dataclasses.dataclass
|
||||
class IV:
|
||||
T_IV4 = dataclasses.InitVar
|
||||
iv0: dataclasses.InitVar[int]
|
||||
iv1: dataclasses.InitVar
|
||||
iv2: T_IV2
|
||||
iv3: T_IV3
|
||||
not_iv4: T_IV4 # When using string annotations, this field is not recognized as an InitVar.
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
USING_STRINGS = True
|
||||
|
||||
# dataclass_module_1.py and dataclass_module_1_str.py are identical
|
||||
# except only the latter uses string annotations.
|
||||
|
||||
import dataclasses
|
||||
import typing
|
||||
|
||||
T_CV2 = typing.ClassVar[int]
|
||||
T_CV3 = typing.ClassVar
|
||||
|
||||
T_IV2 = dataclasses.InitVar[int]
|
||||
T_IV3 = dataclasses.InitVar
|
||||
|
||||
@dataclasses.dataclass
|
||||
class CV:
|
||||
T_CV4 = typing.ClassVar
|
||||
cv0: typing.ClassVar[int] = 20
|
||||
cv1: typing.ClassVar = 30
|
||||
cv2: T_CV2
|
||||
cv3: T_CV3
|
||||
not_cv4: T_CV4 # When using string annotations, this field is not recognized as a ClassVar.
|
||||
|
||||
@dataclasses.dataclass
|
||||
class IV:
|
||||
T_IV4 = dataclasses.InitVar
|
||||
iv0: dataclasses.InitVar[int]
|
||||
iv1: dataclasses.InitVar
|
||||
iv2: T_IV2
|
||||
iv3: T_IV3
|
||||
not_iv4: T_IV4 # When using string annotations, this field is not recognized as an InitVar.
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
#from __future__ import annotations
|
||||
USING_STRINGS = False
|
||||
|
||||
# dataclass_module_2.py and dataclass_module_2_str.py are identical
|
||||
# except only the latter uses string annotations.
|
||||
|
||||
from dataclasses import dataclass, InitVar
|
||||
from typing import ClassVar
|
||||
|
||||
T_CV2 = ClassVar[int]
|
||||
T_CV3 = ClassVar
|
||||
|
||||
T_IV2 = InitVar[int]
|
||||
T_IV3 = InitVar
|
||||
|
||||
@dataclass
|
||||
class CV:
|
||||
T_CV4 = ClassVar
|
||||
cv0: ClassVar[int] = 20
|
||||
cv1: ClassVar = 30
|
||||
cv2: T_CV2
|
||||
cv3: T_CV3
|
||||
not_cv4: T_CV4 # When using string annotations, this field is not recognized as a ClassVar.
|
||||
|
||||
@dataclass
|
||||
class IV:
|
||||
T_IV4 = InitVar
|
||||
iv0: InitVar[int]
|
||||
iv1: InitVar
|
||||
iv2: T_IV2
|
||||
iv3: T_IV3
|
||||
not_iv4: T_IV4 # When using string annotations, this field is not recognized as an InitVar.
|
||||
Vendored
+32
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
USING_STRINGS = True
|
||||
|
||||
# dataclass_module_2.py and dataclass_module_2_str.py are identical
|
||||
# except only the latter uses string annotations.
|
||||
|
||||
from dataclasses import dataclass, InitVar
|
||||
from typing import ClassVar
|
||||
|
||||
T_CV2 = ClassVar[int]
|
||||
T_CV3 = ClassVar
|
||||
|
||||
T_IV2 = InitVar[int]
|
||||
T_IV3 = InitVar
|
||||
|
||||
@dataclass
|
||||
class CV:
|
||||
T_CV4 = ClassVar
|
||||
cv0: ClassVar[int] = 20
|
||||
cv1: ClassVar = 30
|
||||
cv2: T_CV2
|
||||
cv3: T_CV3
|
||||
not_cv4: T_CV4 # When using string annotations, this field is not recognized as a ClassVar.
|
||||
|
||||
@dataclass
|
||||
class IV:
|
||||
T_IV4 = InitVar
|
||||
iv0: InitVar[int]
|
||||
iv1: InitVar
|
||||
iv2: T_IV2
|
||||
iv3: T_IV3
|
||||
not_iv4: T_IV4 # When using string annotations, this field is not recognized as an InitVar.
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Bar:
|
||||
foo: Foo
|
||||
Vendored
+651
-13
@@ -4,12 +4,14 @@
|
||||
|
||||
from dataclasses import *
|
||||
|
||||
import abc
|
||||
import pickle
|
||||
import inspect
|
||||
import builtins
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional
|
||||
from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional, Protocol
|
||||
from typing import get_type_hints
|
||||
from collections import deque, OrderedDict, namedtuple
|
||||
from functools import total_ordering
|
||||
@@ -60,6 +62,7 @@ class TestCase(unittest.TestCase):
|
||||
f"default=1,default_factory={MISSING!r}," \
|
||||
"init=True,repr=False,hash=None," \
|
||||
"compare=True,metadata=mappingproxy({})," \
|
||||
f"kw_only={MISSING!r}," \
|
||||
"_field_type=None)"
|
||||
|
||||
self.assertEqual(repr_output, expected_output)
|
||||
@@ -1124,6 +1127,10 @@ class TestCase(unittest.TestCase):
|
||||
self.assertEqual(repr(InitVar[int]), 'dataclasses.InitVar[int]')
|
||||
self.assertEqual(repr(InitVar[List[int]]),
|
||||
'dataclasses.InitVar[typing.List[int]]')
|
||||
self.assertEqual(repr(InitVar[list[int]]),
|
||||
'dataclasses.InitVar[list[int]]')
|
||||
self.assertEqual(repr(InitVar[int|str]),
|
||||
'dataclasses.InitVar[int | str]')
|
||||
|
||||
def test_init_var_inheritance(self):
|
||||
# Note that this deliberately tests that a dataclass need not
|
||||
@@ -1348,6 +1355,17 @@ class TestCase(unittest.TestCase):
|
||||
with self.assertRaisesRegex(TypeError, 'should be called on dataclass instances'):
|
||||
replace(obj, x=0)
|
||||
|
||||
def test_is_dataclass_genericalias(self):
|
||||
@dataclass
|
||||
class A(types.GenericAlias):
|
||||
origin: type
|
||||
args: type
|
||||
self.assertTrue(is_dataclass(A))
|
||||
a = A(list, int)
|
||||
self.assertTrue(is_dataclass(type(a)))
|
||||
self.assertTrue(is_dataclass(a))
|
||||
|
||||
|
||||
def test_helper_fields_with_class_instance(self):
|
||||
# Check that we can call fields() on either a class or instance,
|
||||
# and get back the same thing.
|
||||
@@ -1907,9 +1925,7 @@ class TestCase(unittest.TestCase):
|
||||
# Check MRO resolution.
|
||||
self.assertEqual(Child.__mro__, (Child, Parent, Generic, object))
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure
|
||||
def test_dataclassses_pickleable(self):
|
||||
def test_dataclasses_pickleable(self):
|
||||
global P, Q, R
|
||||
@dataclass
|
||||
class P:
|
||||
@@ -1938,6 +1954,30 @@ class TestCase(unittest.TestCase):
|
||||
self.assertEqual(new_sample.x, another_new_sample.x)
|
||||
self.assertEqual(sample.y, another_new_sample.y)
|
||||
|
||||
def test_dataclasses_qualnames(self):
|
||||
@dataclass(order=True, unsafe_hash=True, frozen=True)
|
||||
class A:
|
||||
x: int
|
||||
y: int
|
||||
|
||||
self.assertEqual(A.__init__.__name__, "__init__")
|
||||
for function in (
|
||||
'__eq__',
|
||||
'__lt__',
|
||||
'__le__',
|
||||
'__gt__',
|
||||
'__ge__',
|
||||
'__hash__',
|
||||
'__init__',
|
||||
'__repr__',
|
||||
'__setattr__',
|
||||
'__delattr__',
|
||||
):
|
||||
self.assertEqual(getattr(A, function).__qualname__, f"TestCase.test_dataclasses_qualnames.<locals>.A.{function}")
|
||||
|
||||
with self.assertRaisesRegex(TypeError, r"A\.__init__\(\) missing"):
|
||||
A()
|
||||
|
||||
|
||||
class TestFieldNoAnnotation(unittest.TestCase):
|
||||
def test_field_without_annotation(self):
|
||||
@@ -2034,7 +2074,7 @@ class TestDocString(unittest.TestCase):
|
||||
class C:
|
||||
x: Union[int, type(None)] = None
|
||||
|
||||
self.assertDocStrEqual(C.__doc__, "C(x:Union[int, NoneType]=None)")
|
||||
self.assertDocStrEqual(C.__doc__, "C(x:Optional[int]=None)")
|
||||
|
||||
def test_docstring_list_field(self):
|
||||
@dataclass
|
||||
@@ -2134,6 +2174,26 @@ class TestInit(unittest.TestCase):
|
||||
self.x = 2 * x
|
||||
self.assertEqual(C(5).x, 10)
|
||||
|
||||
def test_inherit_from_protocol(self):
|
||||
# Dataclasses inheriting from protocol should preserve their own `__init__`.
|
||||
# See bpo-45081.
|
||||
|
||||
class P(Protocol):
|
||||
a: int
|
||||
|
||||
@dataclass
|
||||
class C(P):
|
||||
a: int
|
||||
|
||||
self.assertEqual(C(5).a, 5)
|
||||
|
||||
@dataclass
|
||||
class D(P):
|
||||
def __init__(self, a):
|
||||
self.a = a * 2
|
||||
|
||||
self.assertEqual(D(5).a, 10)
|
||||
|
||||
|
||||
class TestRepr(unittest.TestCase):
|
||||
def test_repr(self):
|
||||
@@ -2769,6 +2829,107 @@ class TestSlots(unittest.TestCase):
|
||||
# We can add a new field to the derived instance.
|
||||
d.z = 10
|
||||
|
||||
def test_generated_slots(self):
|
||||
@dataclass(slots=True)
|
||||
class C:
|
||||
x: int
|
||||
y: int
|
||||
|
||||
c = C(1, 2)
|
||||
self.assertEqual((c.x, c.y), (1, 2))
|
||||
|
||||
c.x = 3
|
||||
c.y = 4
|
||||
self.assertEqual((c.x, c.y), (3, 4))
|
||||
|
||||
with self.assertRaisesRegex(AttributeError, "'C' object has no attribute 'z'"):
|
||||
c.z = 5
|
||||
|
||||
def test_add_slots_when_slots_exists(self):
|
||||
with self.assertRaisesRegex(TypeError, '^C already specifies __slots__$'):
|
||||
@dataclass(slots=True)
|
||||
class C:
|
||||
__slots__ = ('x',)
|
||||
x: int
|
||||
|
||||
def test_generated_slots_value(self):
|
||||
@dataclass(slots=True)
|
||||
class Base:
|
||||
x: int
|
||||
|
||||
self.assertEqual(Base.__slots__, ('x',))
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Delivered(Base):
|
||||
y: int
|
||||
|
||||
self.assertEqual(Delivered.__slots__, ('x', 'y'))
|
||||
|
||||
@dataclass
|
||||
class AnotherDelivered(Base):
|
||||
z: int
|
||||
|
||||
self.assertTrue('__slots__' not in AnotherDelivered.__dict__)
|
||||
|
||||
def test_returns_new_class(self):
|
||||
class A:
|
||||
x: int
|
||||
|
||||
B = dataclass(A, slots=True)
|
||||
self.assertIsNot(A, B)
|
||||
|
||||
self.assertFalse(hasattr(A, "__slots__"))
|
||||
self.assertTrue(hasattr(B, "__slots__"))
|
||||
|
||||
# Can't be local to test_frozen_pickle.
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FrozenSlotsClass:
|
||||
foo: str
|
||||
bar: int
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrozenWithoutSlotsClass:
|
||||
foo: str
|
||||
bar: int
|
||||
|
||||
def test_frozen_pickle(self):
|
||||
# bpo-43999
|
||||
|
||||
self.assertEqual(self.FrozenSlotsClass.__slots__, ("foo", "bar"))
|
||||
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
|
||||
with self.subTest(proto=proto):
|
||||
obj = self.FrozenSlotsClass("a", 1)
|
||||
p = pickle.loads(pickle.dumps(obj, protocol=proto))
|
||||
self.assertIsNot(obj, p)
|
||||
self.assertEqual(obj, p)
|
||||
|
||||
obj = self.FrozenWithoutSlotsClass("a", 1)
|
||||
p = pickle.loads(pickle.dumps(obj, protocol=proto))
|
||||
self.assertIsNot(obj, p)
|
||||
self.assertEqual(obj, p)
|
||||
|
||||
def test_slots_with_default_no_init(self):
|
||||
# Originally reported in bpo-44649.
|
||||
@dataclass(slots=True)
|
||||
class A:
|
||||
a: str
|
||||
b: str = field(default='b', init=False)
|
||||
|
||||
obj = A("a")
|
||||
self.assertEqual(obj.a, 'a')
|
||||
self.assertEqual(obj.b, 'b')
|
||||
|
||||
def test_slots_with_default_factory_no_init(self):
|
||||
# Originally reported in bpo-44649.
|
||||
@dataclass(slots=True)
|
||||
class A:
|
||||
a: str
|
||||
b: str = field(default_factory=lambda:'b', init=False)
|
||||
|
||||
obj = A("a")
|
||||
self.assertEqual(obj.a, 'a')
|
||||
self.assertEqual(obj.b, 'b')
|
||||
|
||||
class TestDescriptors(unittest.TestCase):
|
||||
def test_set_name(self):
|
||||
# See bpo-33141.
|
||||
@@ -2840,6 +3001,115 @@ class TestDescriptors(unittest.TestCase):
|
||||
|
||||
self.assertEqual(D.__set_name__.call_count, 1)
|
||||
|
||||
def test_init_calls_set(self):
|
||||
class D:
|
||||
pass
|
||||
|
||||
D.__set__ = Mock()
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
i: D = D()
|
||||
|
||||
# Make sure D.__set__ is called.
|
||||
D.__set__.reset_mock()
|
||||
c = C(5)
|
||||
self.assertEqual(D.__set__.call_count, 1)
|
||||
|
||||
def test_getting_field_calls_get(self):
|
||||
class D:
|
||||
pass
|
||||
|
||||
D.__set__ = Mock()
|
||||
D.__get__ = Mock()
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
i: D = D()
|
||||
|
||||
c = C(5)
|
||||
|
||||
# Make sure D.__get__ is called.
|
||||
D.__get__.reset_mock()
|
||||
value = c.i
|
||||
self.assertEqual(D.__get__.call_count, 1)
|
||||
|
||||
def test_setting_field_calls_set(self):
|
||||
class D:
|
||||
pass
|
||||
|
||||
D.__set__ = Mock()
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
i: D = D()
|
||||
|
||||
c = C(5)
|
||||
|
||||
# Make sure D.__set__ is called.
|
||||
D.__set__.reset_mock()
|
||||
c.i = 10
|
||||
self.assertEqual(D.__set__.call_count, 1)
|
||||
|
||||
def test_setting_uninitialized_descriptor_field(self):
|
||||
class D:
|
||||
pass
|
||||
|
||||
D.__set__ = Mock()
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
i: D
|
||||
|
||||
# D.__set__ is not called because there's no D instance to call it on
|
||||
D.__set__.reset_mock()
|
||||
c = C(5)
|
||||
self.assertEqual(D.__set__.call_count, 0)
|
||||
|
||||
# D.__set__ still isn't called after setting i to an instance of D
|
||||
# because descriptors don't behave like that when stored as instance vars
|
||||
c.i = D()
|
||||
c.i = 5
|
||||
self.assertEqual(D.__set__.call_count, 0)
|
||||
|
||||
def test_default_value(self):
|
||||
class D:
|
||||
def __get__(self, instance: Any, owner: object) -> int:
|
||||
if instance is None:
|
||||
return 100
|
||||
|
||||
return instance._x
|
||||
|
||||
def __set__(self, instance: Any, value: int) -> None:
|
||||
instance._x = value
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
i: D = D()
|
||||
|
||||
c = C()
|
||||
self.assertEqual(c.i, 100)
|
||||
|
||||
c = C(5)
|
||||
self.assertEqual(c.i, 5)
|
||||
|
||||
def test_no_default_value(self):
|
||||
class D:
|
||||
def __get__(self, instance: Any, owner: object) -> int:
|
||||
if instance is None:
|
||||
raise AttributeError()
|
||||
|
||||
return instance._x
|
||||
|
||||
def __set__(self, instance: Any, value: int) -> None:
|
||||
instance._x = value
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
i: D = D()
|
||||
|
||||
with self.assertRaisesRegex(TypeError, 'missing 1 required positional argument'):
|
||||
c = C()
|
||||
|
||||
class TestStringAnnotations(unittest.TestCase):
|
||||
def test_classvar(self):
|
||||
@@ -2849,7 +3119,7 @@ class TestStringAnnotations(unittest.TestCase):
|
||||
# These tests assume that both "import typing" and "from
|
||||
# typing import *" have been run in this file.
|
||||
for typestr in ('ClassVar[int]',
|
||||
'ClassVar [int]'
|
||||
'ClassVar [int]',
|
||||
' ClassVar [int]',
|
||||
'ClassVar',
|
||||
' ClassVar ',
|
||||
@@ -3176,13 +3446,6 @@ class TestReplace(unittest.TestCase):
|
||||
self.assertEqual(c1.x, 3)
|
||||
self.assertEqual(c1.y, 2)
|
||||
|
||||
self.assertRaises(TypeError, replace)
|
||||
self.assertRaises(TypeError, replace, c, c)
|
||||
with self.assertWarns(DeprecationWarning):
|
||||
c1 = replace(obj=c, x=3)
|
||||
self.assertEqual(c1.x, 3)
|
||||
self.assertEqual(c1.y, 2)
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure
|
||||
def test_frozen(self):
|
||||
@@ -3405,6 +3668,381 @@ class TestReplace(unittest.TestCase):
|
||||
|
||||
## replace(c, x=5)
|
||||
|
||||
class TestAbstract(unittest.TestCase):
|
||||
def test_abc_implementation(self):
|
||||
class Ordered(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def __lt__(self, other):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def __le__(self, other):
|
||||
pass
|
||||
|
||||
@dataclass(order=True)
|
||||
class Date(Ordered):
|
||||
year: int
|
||||
month: 'Month'
|
||||
day: 'int'
|
||||
|
||||
self.assertFalse(inspect.isabstract(Date))
|
||||
self.assertGreater(Date(2020,12,25), Date(2020,8,31))
|
||||
|
||||
def test_maintain_abc(self):
|
||||
class A(abc.ABC):
|
||||
@abc.abstractmethod
|
||||
def foo(self):
|
||||
pass
|
||||
|
||||
@dataclass
|
||||
class Date(A):
|
||||
year: int
|
||||
month: 'Month'
|
||||
day: 'int'
|
||||
|
||||
self.assertTrue(inspect.isabstract(Date))
|
||||
msg = 'class Date with abstract method foo'
|
||||
self.assertRaisesRegex(TypeError, msg, Date)
|
||||
|
||||
|
||||
class TestMatchArgs(unittest.TestCase):
|
||||
def test_match_args(self):
|
||||
@dataclass
|
||||
class C:
|
||||
a: int
|
||||
self.assertEqual(C(42).__match_args__, ('a',))
|
||||
|
||||
def test_explicit_match_args(self):
|
||||
ma = ()
|
||||
@dataclass
|
||||
class C:
|
||||
a: int
|
||||
__match_args__ = ma
|
||||
self.assertIs(C(42).__match_args__, ma)
|
||||
|
||||
def test_bpo_43764(self):
|
||||
@dataclass(repr=False, eq=False, init=False)
|
||||
class X:
|
||||
a: int
|
||||
b: int
|
||||
c: int
|
||||
self.assertEqual(X.__match_args__, ("a", "b", "c"))
|
||||
|
||||
def test_match_args_argument(self):
|
||||
@dataclass(match_args=False)
|
||||
class X:
|
||||
a: int
|
||||
self.assertNotIn('__match_args__', X.__dict__)
|
||||
|
||||
@dataclass(match_args=False)
|
||||
class Y:
|
||||
a: int
|
||||
__match_args__ = ('b',)
|
||||
self.assertEqual(Y.__match_args__, ('b',))
|
||||
|
||||
@dataclass(match_args=False)
|
||||
class Z(Y):
|
||||
z: int
|
||||
self.assertEqual(Z.__match_args__, ('b',))
|
||||
|
||||
# Ensure parent dataclass __match_args__ is seen, if child class
|
||||
# specifies match_args=False.
|
||||
@dataclass
|
||||
class A:
|
||||
a: int
|
||||
z: int
|
||||
@dataclass(match_args=False)
|
||||
class B(A):
|
||||
b: int
|
||||
self.assertEqual(B.__match_args__, ('a', 'z'))
|
||||
|
||||
def test_make_dataclasses(self):
|
||||
C = make_dataclass('C', [('x', int), ('y', int)])
|
||||
self.assertEqual(C.__match_args__, ('x', 'y'))
|
||||
|
||||
C = make_dataclass('C', [('x', int), ('y', int)], match_args=True)
|
||||
self.assertEqual(C.__match_args__, ('x', 'y'))
|
||||
|
||||
C = make_dataclass('C', [('x', int), ('y', int)], match_args=False)
|
||||
self.assertNotIn('__match__args__', C.__dict__)
|
||||
|
||||
C = make_dataclass('C', [('x', int), ('y', int)], namespace={'__match_args__': ('z',)})
|
||||
self.assertEqual(C.__match_args__, ('z',))
|
||||
|
||||
|
||||
class TestKeywordArgs(unittest.TestCase):
|
||||
def test_no_classvar_kwarg(self):
|
||||
msg = 'field a is a ClassVar but specifies kw_only'
|
||||
with self.assertRaisesRegex(TypeError, msg):
|
||||
@dataclass
|
||||
class A:
|
||||
a: ClassVar[int] = field(kw_only=True)
|
||||
|
||||
with self.assertRaisesRegex(TypeError, msg):
|
||||
@dataclass
|
||||
class A:
|
||||
a: ClassVar[int] = field(kw_only=False)
|
||||
|
||||
with self.assertRaisesRegex(TypeError, msg):
|
||||
@dataclass(kw_only=True)
|
||||
class A:
|
||||
a: ClassVar[int] = field(kw_only=False)
|
||||
|
||||
def test_field_marked_as_kwonly(self):
|
||||
#######################
|
||||
# Using dataclass(kw_only=True)
|
||||
@dataclass(kw_only=True)
|
||||
class A:
|
||||
a: int
|
||||
self.assertTrue(fields(A)[0].kw_only)
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class A:
|
||||
a: int = field(kw_only=True)
|
||||
self.assertTrue(fields(A)[0].kw_only)
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class A:
|
||||
a: int = field(kw_only=False)
|
||||
self.assertFalse(fields(A)[0].kw_only)
|
||||
|
||||
#######################
|
||||
# Using dataclass(kw_only=False)
|
||||
@dataclass(kw_only=False)
|
||||
class A:
|
||||
a: int
|
||||
self.assertFalse(fields(A)[0].kw_only)
|
||||
|
||||
@dataclass(kw_only=False)
|
||||
class A:
|
||||
a: int = field(kw_only=True)
|
||||
self.assertTrue(fields(A)[0].kw_only)
|
||||
|
||||
@dataclass(kw_only=False)
|
||||
class A:
|
||||
a: int = field(kw_only=False)
|
||||
self.assertFalse(fields(A)[0].kw_only)
|
||||
|
||||
#######################
|
||||
# Not specifying dataclass(kw_only)
|
||||
@dataclass
|
||||
class A:
|
||||
a: int
|
||||
self.assertFalse(fields(A)[0].kw_only)
|
||||
|
||||
@dataclass
|
||||
class A:
|
||||
a: int = field(kw_only=True)
|
||||
self.assertTrue(fields(A)[0].kw_only)
|
||||
|
||||
@dataclass
|
||||
class A:
|
||||
a: int = field(kw_only=False)
|
||||
self.assertFalse(fields(A)[0].kw_only)
|
||||
|
||||
def test_match_args(self):
|
||||
# kw fields don't show up in __match_args__.
|
||||
@dataclass(kw_only=True)
|
||||
class C:
|
||||
a: int
|
||||
self.assertEqual(C(a=42).__match_args__, ())
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
a: int
|
||||
b: int = field(kw_only=True)
|
||||
self.assertEqual(C(42, b=10).__match_args__, ('a',))
|
||||
|
||||
def test_KW_ONLY(self):
|
||||
@dataclass
|
||||
class A:
|
||||
a: int
|
||||
_: KW_ONLY
|
||||
b: int
|
||||
c: int
|
||||
A(3, c=5, b=4)
|
||||
msg = "takes 2 positional arguments but 4 were given"
|
||||
with self.assertRaisesRegex(TypeError, msg):
|
||||
A(3, 4, 5)
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class B:
|
||||
a: int
|
||||
_: KW_ONLY
|
||||
b: int
|
||||
c: int
|
||||
B(a=3, b=4, c=5)
|
||||
msg = "takes 1 positional argument but 4 were given"
|
||||
with self.assertRaisesRegex(TypeError, msg):
|
||||
B(3, 4, 5)
|
||||
|
||||
# Explicitly make a field that follows KW_ONLY be non-keyword-only.
|
||||
@dataclass
|
||||
class C:
|
||||
a: int
|
||||
_: KW_ONLY
|
||||
b: int
|
||||
c: int = field(kw_only=False)
|
||||
c = C(1, 2, b=3)
|
||||
self.assertEqual(c.a, 1)
|
||||
self.assertEqual(c.b, 3)
|
||||
self.assertEqual(c.c, 2)
|
||||
c = C(1, b=3, c=2)
|
||||
self.assertEqual(c.a, 1)
|
||||
self.assertEqual(c.b, 3)
|
||||
self.assertEqual(c.c, 2)
|
||||
c = C(1, b=3, c=2)
|
||||
self.assertEqual(c.a, 1)
|
||||
self.assertEqual(c.b, 3)
|
||||
self.assertEqual(c.c, 2)
|
||||
c = C(c=2, b=3, a=1)
|
||||
self.assertEqual(c.a, 1)
|
||||
self.assertEqual(c.b, 3)
|
||||
self.assertEqual(c.c, 2)
|
||||
|
||||
def test_KW_ONLY_as_string(self):
|
||||
@dataclass
|
||||
class A:
|
||||
a: int
|
||||
_: 'dataclasses.KW_ONLY'
|
||||
b: int
|
||||
c: int
|
||||
A(3, c=5, b=4)
|
||||
msg = "takes 2 positional arguments but 4 were given"
|
||||
with self.assertRaisesRegex(TypeError, msg):
|
||||
A(3, 4, 5)
|
||||
|
||||
def test_KW_ONLY_twice(self):
|
||||
msg = "'Y' is KW_ONLY, but KW_ONLY has already been specified"
|
||||
|
||||
with self.assertRaisesRegex(TypeError, msg):
|
||||
@dataclass
|
||||
class A:
|
||||
a: int
|
||||
X: KW_ONLY
|
||||
Y: KW_ONLY
|
||||
b: int
|
||||
c: int
|
||||
|
||||
with self.assertRaisesRegex(TypeError, msg):
|
||||
@dataclass
|
||||
class A:
|
||||
a: int
|
||||
X: KW_ONLY
|
||||
b: int
|
||||
Y: KW_ONLY
|
||||
c: int
|
||||
|
||||
with self.assertRaisesRegex(TypeError, msg):
|
||||
@dataclass
|
||||
class A:
|
||||
a: int
|
||||
X: KW_ONLY
|
||||
b: int
|
||||
c: int
|
||||
Y: KW_ONLY
|
||||
|
||||
# But this usage is okay, since it's not using KW_ONLY.
|
||||
@dataclass
|
||||
class A:
|
||||
a: int
|
||||
_: KW_ONLY
|
||||
b: int
|
||||
c: int = field(kw_only=True)
|
||||
|
||||
# And if inheriting, it's okay.
|
||||
@dataclass
|
||||
class A:
|
||||
a: int
|
||||
_: KW_ONLY
|
||||
b: int
|
||||
c: int
|
||||
@dataclass
|
||||
class B(A):
|
||||
_: KW_ONLY
|
||||
d: int
|
||||
|
||||
# Make sure the error is raised in a derived class.
|
||||
with self.assertRaisesRegex(TypeError, msg):
|
||||
@dataclass
|
||||
class A:
|
||||
a: int
|
||||
_: KW_ONLY
|
||||
b: int
|
||||
c: int
|
||||
@dataclass
|
||||
class B(A):
|
||||
X: KW_ONLY
|
||||
d: int
|
||||
Y: KW_ONLY
|
||||
|
||||
|
||||
def test_post_init(self):
|
||||
@dataclass
|
||||
class A:
|
||||
a: int
|
||||
_: KW_ONLY
|
||||
b: InitVar[int]
|
||||
c: int
|
||||
d: InitVar[int]
|
||||
def __post_init__(self, b, d):
|
||||
raise CustomError(f'{b=} {d=}')
|
||||
with self.assertRaisesRegex(CustomError, 'b=3 d=4'):
|
||||
A(1, c=2, b=3, d=4)
|
||||
|
||||
@dataclass
|
||||
class B:
|
||||
a: int
|
||||
_: KW_ONLY
|
||||
b: InitVar[int]
|
||||
c: int
|
||||
d: InitVar[int]
|
||||
def __post_init__(self, b, d):
|
||||
self.a = b
|
||||
self.c = d
|
||||
b = B(1, c=2, b=3, d=4)
|
||||
self.assertEqual(asdict(b), {'a': 3, 'c': 4})
|
||||
|
||||
def test_defaults(self):
|
||||
# For kwargs, make sure we can have defaults after non-defaults.
|
||||
@dataclass
|
||||
class A:
|
||||
a: int = 0
|
||||
_: KW_ONLY
|
||||
b: int
|
||||
c: int = 1
|
||||
d: int
|
||||
|
||||
a = A(d=4, b=3)
|
||||
self.assertEqual(a.a, 0)
|
||||
self.assertEqual(a.b, 3)
|
||||
self.assertEqual(a.c, 1)
|
||||
self.assertEqual(a.d, 4)
|
||||
|
||||
# Make sure we still check for non-kwarg non-defaults not following
|
||||
# defaults.
|
||||
err_regex = "non-default argument 'z' follows default argument"
|
||||
with self.assertRaisesRegex(TypeError, err_regex):
|
||||
@dataclass
|
||||
class A:
|
||||
a: int = 0
|
||||
z: int
|
||||
_: KW_ONLY
|
||||
b: int
|
||||
c: int = 1
|
||||
d: int
|
||||
|
||||
def test_make_dataclass(self):
|
||||
A = make_dataclass("A", ['a'], kw_only=True)
|
||||
self.assertTrue(fields(A)[0].kw_only)
|
||||
|
||||
B = make_dataclass("B",
|
||||
['a', ('b', int, field(kw_only=False))],
|
||||
kw_only=True)
|
||||
self.assertTrue(fields(B)[0].kw_only)
|
||||
self.assertFalse(fields(B)[1].kw_only)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user