diff -r 7e6f3c69c0fb -r e1c586b9a43c mercurial/thirdparty/attr/_funcs.py --- a/mercurial/thirdparty/attr/_funcs.py Mon Nov 21 16:18:28 2022 -0500 +++ b/mercurial/thirdparty/attr/_funcs.py Mon Nov 21 15:04:42 2022 -0500 @@ -1,14 +1,20 @@ -from __future__ import absolute_import, division, print_function +# SPDX-License-Identifier: MIT + import copy -from ._compat import iteritems -from ._make import NOTHING, fields, _obj_setattr +from ._make import NOTHING, _obj_setattr, fields from .exceptions import AttrsAttributeNotFoundError -def asdict(inst, recurse=True, filter=None, dict_factory=dict, - retain_collection_types=False): +def asdict( + inst, + recurse=True, + filter=None, + dict_factory=dict, + retain_collection_types=False, + value_serializer=None, +): """ Return the ``attrs`` attribute values of *inst* as a dict. @@ -17,9 +23,9 @@ :param inst: Instance of an ``attrs``-decorated class. :param bool recurse: Recurse into classes that are also ``attrs``-decorated. - :param callable filter: A callable whose return code deteremines whether an + :param callable filter: A callable whose return code determines whether an attribute or element is included (``True``) or dropped (``False``). Is - called with the :class:`attr.Attribute` as the first argument and the + called with the `attrs.Attribute` as the first argument and the value as the second argument. :param callable dict_factory: A callable to produce dictionaries from. For example, to produce ordered dictionaries instead of normal Python @@ -27,6 +33,10 @@ :param bool retain_collection_types: Do not convert to ``list`` when encountering an attribute whose type is ``tuple`` or ``set``. Only meaningful if ``recurse`` is ``True``. + :param Optional[callable] value_serializer: A hook that is called for every + attribute or dict key/value. It receives the current instance, field + and value and must return the (updated) value. The hook is run *after* + the optional *filter* has been applied. :rtype: return type of *dict_factory* @@ -35,6 +45,9 @@ .. versionadded:: 16.0.0 *dict_factory* .. versionadded:: 16.1.0 *retain_collection_types* + .. versionadded:: 20.3.0 *value_serializer* + .. versionadded:: 21.3.0 If a dict has a collection for a key, it is + serialized as a tuple. """ attrs = fields(inst.__class__) rv = dict_factory() @@ -42,24 +55,58 @@ v = getattr(inst, a.name) if filter is not None and not filter(a, v): continue + + if value_serializer is not None: + v = value_serializer(inst, a, v) + if recurse is True: if has(v.__class__): - rv[a.name] = asdict(v, recurse=True, filter=filter, - dict_factory=dict_factory) - elif isinstance(v, (tuple, list, set)): + rv[a.name] = asdict( + v, + recurse=True, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + elif isinstance(v, (tuple, list, set, frozenset)): cf = v.__class__ if retain_collection_types is True else list - rv[a.name] = cf([ - asdict(i, recurse=True, filter=filter, - dict_factory=dict_factory) - if has(i.__class__) else i - for i in v - ]) + rv[a.name] = cf( + [ + _asdict_anything( + i, + is_key=False, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + for i in v + ] + ) elif isinstance(v, dict): df = dict_factory - rv[a.name] = df(( - asdict(kk, dict_factory=df) if has(kk.__class__) else kk, - asdict(vv, dict_factory=df) if has(vv.__class__) else vv) - for kk, vv in iteritems(v)) + rv[a.name] = df( + ( + _asdict_anything( + kk, + is_key=True, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + _asdict_anything( + vv, + is_key=False, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + ) + for kk, vv in v.items() + ) else: rv[a.name] = v else: @@ -67,8 +114,86 @@ return rv -def astuple(inst, recurse=True, filter=None, tuple_factory=tuple, - retain_collection_types=False): +def _asdict_anything( + val, + is_key, + filter, + dict_factory, + retain_collection_types, + value_serializer, +): + """ + ``asdict`` only works on attrs instances, this works on anything. + """ + if getattr(val.__class__, "__attrs_attrs__", None) is not None: + # Attrs class. + rv = asdict( + val, + recurse=True, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + elif isinstance(val, (tuple, list, set, frozenset)): + if retain_collection_types is True: + cf = val.__class__ + elif is_key: + cf = tuple + else: + cf = list + + rv = cf( + [ + _asdict_anything( + i, + is_key=False, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + for i in val + ] + ) + elif isinstance(val, dict): + df = dict_factory + rv = df( + ( + _asdict_anything( + kk, + is_key=True, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + _asdict_anything( + vv, + is_key=False, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + ) + for kk, vv in val.items() + ) + else: + rv = val + if value_serializer is not None: + rv = value_serializer(None, None, rv) + + return rv + + +def astuple( + inst, + recurse=True, + filter=None, + tuple_factory=tuple, + retain_collection_types=False, +): """ Return the ``attrs`` attribute values of *inst* as a tuple. @@ -79,7 +204,7 @@ ``attrs``-decorated. :param callable filter: A callable whose return code determines whether an attribute or element is included (``True``) or dropped (``False``). Is - called with the :class:`attr.Attribute` as the first argument and the + called with the `attrs.Attribute` as the first argument and the value as the second argument. :param callable tuple_factory: A callable to produce tuples from. For example, to produce lists instead of tuples. @@ -104,38 +229,61 @@ continue if recurse is True: if has(v.__class__): - rv.append(astuple(v, recurse=True, filter=filter, - tuple_factory=tuple_factory, - retain_collection_types=retain)) - elif isinstance(v, (tuple, list, set)): + rv.append( + astuple( + v, + recurse=True, + filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + ) + elif isinstance(v, (tuple, list, set, frozenset)): cf = v.__class__ if retain is True else list - rv.append(cf([ - astuple(j, recurse=True, filter=filter, - tuple_factory=tuple_factory, - retain_collection_types=retain) - if has(j.__class__) else j - for j in v - ])) + rv.append( + cf( + [ + astuple( + j, + recurse=True, + filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(j.__class__) + else j + for j in v + ] + ) + ) elif isinstance(v, dict): df = v.__class__ if retain is True else dict - rv.append(df( + rv.append( + df( ( astuple( kk, tuple_factory=tuple_factory, - retain_collection_types=retain - ) if has(kk.__class__) else kk, + retain_collection_types=retain, + ) + if has(kk.__class__) + else kk, astuple( vv, tuple_factory=tuple_factory, - retain_collection_types=retain - ) if has(vv.__class__) else vv + retain_collection_types=retain, + ) + if has(vv.__class__) + else vv, ) - for kk, vv in iteritems(v))) + for kk, vv in v.items() + ) + ) else: rv.append(v) else: rv.append(v) + return rv if tuple_factory is list else tuple_factory(rv) @@ -146,7 +294,7 @@ :param type cls: Class to introspect. :raise TypeError: If *cls* is not a class. - :rtype: :class:`bool` + :rtype: bool """ return getattr(cls, "__attrs_attrs__", None) is not None @@ -166,19 +314,26 @@ class. .. deprecated:: 17.1.0 - Use :func:`evolve` instead. + Use `attrs.evolve` instead if you can. + This function will not be removed du to the slightly different approach + compared to `attrs.evolve`. """ import warnings - warnings.warn("assoc is deprecated and will be removed after 2018/01.", - DeprecationWarning) + + warnings.warn( + "assoc is deprecated and will be removed after 2018/01.", + DeprecationWarning, + stacklevel=2, + ) new = copy.copy(inst) attrs = fields(inst.__class__) - for k, v in iteritems(changes): + for k, v in changes.items(): a = getattr(attrs, k, NOTHING) if a is NOTHING: raise AttrsAttributeNotFoundError( - "{k} is not an attrs attribute on {cl}." - .format(k=k, cl=new.__class__) + "{k} is not an attrs attribute on {cl}.".format( + k=k, cl=new.__class__ + ) ) _obj_setattr(new, k, v) return new @@ -209,4 +364,57 @@ init_name = attr_name if attr_name[0] != "_" else attr_name[1:] if init_name not in changes: changes[init_name] = getattr(inst, attr_name) + return cls(**changes) + + +def resolve_types(cls, globalns=None, localns=None, attribs=None): + """ + Resolve any strings and forward annotations in type annotations. + + This is only required if you need concrete types in `Attribute`'s *type* + field. In other words, you don't need to resolve your types if you only + use them for static type checking. + + With no arguments, names will be looked up in the module in which the class + was created. If this is not what you want, e.g. if the name only exists + inside a method, you may pass *globalns* or *localns* to specify other + dictionaries in which to look up these names. See the docs of + `typing.get_type_hints` for more details. + + :param type cls: Class to resolve. + :param Optional[dict] globalns: Dictionary containing global variables. + :param Optional[dict] localns: Dictionary containing local variables. + :param Optional[list] attribs: List of attribs for the given class. + This is necessary when calling from inside a ``field_transformer`` + since *cls* is not an ``attrs`` class yet. + + :raise TypeError: If *cls* is not a class. + :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs`` + class and you didn't pass any attribs. + :raise NameError: If types cannot be resolved because of missing variables. + + :returns: *cls* so you can use this function also as a class decorator. + Please note that you have to apply it **after** `attrs.define`. That + means the decorator has to come in the line **before** `attrs.define`. + + .. versionadded:: 20.1.0 + .. versionadded:: 21.1.0 *attribs* + + """ + # Since calling get_type_hints is expensive we cache whether we've + # done it already. + if getattr(cls, "__attrs_types_resolved__", None) != cls: + import typing + + hints = typing.get_type_hints(cls, globalns=globalns, localns=localns) + for field in fields(cls) if attribs is None else attribs: + if field.name in hints: + # Since fields have been frozen we must work around it. + _obj_setattr(field, "type", hints[field.name]) + # We store the class we resolved so that subclasses know they haven't + # been resolved. + cls.__attrs_types_resolved__ = cls + + # Return the class so you can use it as a decorator too. + return cls