diff options
| author | shadchin <[email protected]> | 2022-04-18 12:39:32 +0300 |
|---|---|---|
| committer | shadchin <[email protected]> | 2022-04-18 12:39:32 +0300 |
| commit | d4be68e361f4258cf0848fc70018dfe37a2acc24 (patch) | |
| tree | 153e294cd97ac8b5d7a989612704a0c1f58e8ad4 /contrib/tools/python3/src/Lib/dataclasses.py | |
| parent | 260c02f5ccf242d9d9b8a873afaf6588c00237d6 (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.py | 264 |
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): |
