summaryrefslogtreecommitdiffstats
path: root/contrib/tools/python3/src/Lib/dataclasses.py
diff options
context:
space:
mode:
authorshadchin <[email protected]>2022-04-18 12:39:32 +0300
committershadchin <[email protected]>2022-04-18 12:39:32 +0300
commitd4be68e361f4258cf0848fc70018dfe37a2acc24 (patch)
tree153e294cd97ac8b5d7a989612704a0c1f58e8ad4 /contrib/tools/python3/src/Lib/dataclasses.py
parent260c02f5ccf242d9d9b8a873afaf6588c00237d6 (diff)
IGNIETFERRO-1816 Update Python 3 from 3.9.12 to 3.10.4
ref:9f96be6d02ee8044fdd6f124b799b270c20ce641
Diffstat (limited to 'contrib/tools/python3/src/Lib/dataclasses.py')
-rw-r--r--contrib/tools/python3/src/Lib/dataclasses.py264
1 files changed, 217 insertions, 47 deletions
diff --git a/contrib/tools/python3/src/Lib/dataclasses.py b/contrib/tools/python3/src/Lib/dataclasses.py
index 5ff67ad2ea9..105a95b9554 100644
--- a/contrib/tools/python3/src/Lib/dataclasses.py
+++ b/contrib/tools/python3/src/Lib/dataclasses.py
@@ -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({})
@@ -217,7 +239,6 @@ class InitVar:
def __class_getitem__(cls, type):
return InitVar(type)
-
# Instances of Field are only ever created from within this module,
# and only from the field() function, although Field instances are
# exposed externally as (conceptually) read-only objects.
@@ -238,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
@@ -254,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):
@@ -267,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}'
')')
@@ -320,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.
"""
@@ -338,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):
@@ -395,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)
@@ -413,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.
@@ -453,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
@@ -486,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
@@ -494,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
@@ -512,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:
@@ -528,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,
@@ -608,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
@@ -668,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().
@@ -742,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 '
@@ -749,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
@@ -769,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.
@@ -806,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
@@ -860,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
@@ -916,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"
@@ -933,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
@@ -992,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.
@@ -1006,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:
@@ -1169,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
@@ -1195,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
@@ -1211,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}')
@@ -1223,14 +1385,22 @@ 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(obj, /, **changes):