Update dataclasses from CPython 3.10.6

This commit is contained in:
CPython Developers authored and Jeong YunWon committed 2022-08-15 04:24:58 +09:00
1 parent 945bbe9bf6
commit 330981c533
7 files changed
+1013 -80

No files matched your search

+222 -67
View File
@@ -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)'
+32
View File
@@ -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.
+32
View File
@@ -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.
+32
View File
@@ -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.
+32
View File
@@ -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.
+12
View File
@@ -0,0 +1,12 @@
from __future__ import annotations
import dataclasses
class Foo:
pass
@dataclasses.dataclass
class Bar:
foo: Foo
+651 -13
View File
@@ -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()