##// END OF EJS Templates
interfaces: introduce and use a protocol class for the `bdiff` module...
interfaces: introduce and use a protocol class for the `bdiff` module This is allowed by PEP 544[1], and we basically follow the example there. The class here is copied from `mercurial.pure.bdiff`, and the implementation removed. There are several modules that have a few different implementations, and the implementation chosen is controlled by `HGMODULEPOLICY`. The module is loaded via `mercurial/policy.py`, and has been inferred by pytype as `Any` up to this point. Therefore it and PyCharm were blind to all functions on the module, and their signatures. Also, having multiple instances of the same module allows their signatures to get out of sync. Introducing a protocol class allows the loaded module that is stored in a variable to be given type info, which cascades through the various places it is used. This change alters 11 *.pyi files, for example. In theory, this would also allow us to ensure the various implementations of the same module are kept in alignment- simply import the module in a test module, attempt to pass it to a function that uses the corresponding protocol as an argument, and run pytype on it. In practice, this doesn't work (yet). PyCharm (erroneously) flags imported modules being passed where a protocol class is used[2]. Pytype has problems the other way- it fails to detect when a module that doesn't adhere to the protocol is passed to a protocol argument. The good news is that mypy properly detects this case. The bad news is that mypy spews a bunch of other errors when importing even simple modules, like the various `bdiff` modules. Therefore I'm punting on the tests for now because the type info around a loaded module in PyCharm is a clear win by itself. [1] https://peps.python.org/pep-0544/#modules-as-implementations-of-protocols [2] https://youtrack.jetbrains.com/issue/PY-58679/Support-modules-implementing-protocols

File last commit:

r50538:e1c586b9 default
r52826:f2832de2 default
Show More
_funcs.py
420 lines | 14.3 KiB | text/x-python | PythonLexer
Matt Harbison
attr: vendor 22.1.0...
r50538 # SPDX-License-Identifier: MIT
Siddharth Agarwal
thirdparty: vendor attrs...
r34398
import copy
Matt Harbison
attr: vendor 22.1.0...
r50538 from ._make import NOTHING, _obj_setattr, fields
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 from .exceptions import AttrsAttributeNotFoundError
Matt Harbison
attr: vendor 22.1.0...
r50538 def asdict(
inst,
recurse=True,
filter=None,
dict_factory=dict,
retain_collection_types=False,
value_serializer=None,
):
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 """
Return the ``attrs`` attribute values of *inst* as a dict.
Optionally recurse into other ``attrs``-decorated classes.
:param inst: Instance of an ``attrs``-decorated class.
:param bool recurse: Recurse into classes that are also
``attrs``-decorated.
Matt Harbison
attr: vendor 22.1.0...
r50538 :param callable filter: A callable whose return code determines whether an
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 attribute or element is included (``True``) or dropped (``False``). Is
Matt Harbison
attr: vendor 22.1.0...
r50538 called with the `attrs.Attribute` as the first argument and the
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 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
dictionaries, pass in ``collections.OrderedDict``.
: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``.
Matt Harbison
attr: vendor 22.1.0...
r50538 :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.
Siddharth Agarwal
thirdparty: vendor attrs...
r34398
:rtype: return type of *dict_factory*
:raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
class.
.. versionadded:: 16.0.0 *dict_factory*
.. versionadded:: 16.1.0 *retain_collection_types*
Matt Harbison
attr: vendor 22.1.0...
r50538 .. 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.
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 """
attrs = fields(inst.__class__)
rv = dict_factory()
for a in attrs:
v = getattr(inst, a.name)
if filter is not None and not filter(a, v):
continue
Matt Harbison
attr: vendor 22.1.0...
r50538
if value_serializer is not None:
v = value_serializer(inst, a, v)
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 if recurse is True:
if has(v.__class__):
Matt Harbison
attr: vendor 22.1.0...
r50538 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)):
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 cf = v.__class__ if retain_collection_types is True else list
Matt Harbison
attr: vendor 22.1.0...
r50538 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
]
)
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 elif isinstance(v, dict):
df = dict_factory
Matt Harbison
attr: vendor 22.1.0...
r50538 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()
)
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 else:
rv[a.name] = v
else:
rv[a.name] = v
return rv
Matt Harbison
attr: vendor 22.1.0...
r50538 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,
):
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 """
Return the ``attrs`` attribute values of *inst* as a tuple.
Optionally recurse into other ``attrs``-decorated classes.
: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 determines whether an
attribute or element is included (``True``) or dropped (``False``). Is
Matt Harbison
attr: vendor 22.1.0...
r50538 called with the `attrs.Attribute` as the first argument and the
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 value as the second argument.
:param callable tuple_factory: A callable to produce tuples from. For
example, to produce lists instead of tuples.
:param bool retain_collection_types: Do not convert to ``list``
or ``dict`` when encountering an attribute which type is
``tuple``, ``dict`` or ``set``. Only meaningful if ``recurse`` is
``True``.
:rtype: return type of *tuple_factory*
:raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
class.
.. versionadded:: 16.2.0
"""
attrs = fields(inst.__class__)
rv = []
retain = retain_collection_types # Very long. :/
for a in attrs:
v = getattr(inst, a.name)
if filter is not None and not filter(a, v):
continue
if recurse is True:
if has(v.__class__):
Matt Harbison
attr: vendor 22.1.0...
r50538 rv.append(
astuple(
v,
recurse=True,
filter=filter,
tuple_factory=tuple_factory,
retain_collection_types=retain,
)
)
elif isinstance(v, (tuple, list, set, frozenset)):
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 cf = v.__class__ if retain is True else list
Matt Harbison
attr: vendor 22.1.0...
r50538 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
]
)
)
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 elif isinstance(v, dict):
df = v.__class__ if retain is True else dict
Matt Harbison
attr: vendor 22.1.0...
r50538 rv.append(
df(
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 (
astuple(
kk,
tuple_factory=tuple_factory,
Matt Harbison
attr: vendor 22.1.0...
r50538 retain_collection_types=retain,
)
if has(kk.__class__)
else kk,
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 astuple(
vv,
tuple_factory=tuple_factory,
Matt Harbison
attr: vendor 22.1.0...
r50538 retain_collection_types=retain,
)
if has(vv.__class__)
else vv,
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 )
Matt Harbison
attr: vendor 22.1.0...
r50538 for kk, vv in v.items()
)
)
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 else:
rv.append(v)
else:
rv.append(v)
Matt Harbison
attr: vendor 22.1.0...
r50538
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 return rv if tuple_factory is list else tuple_factory(rv)
def has(cls):
"""
Check whether *cls* is a class with ``attrs`` attributes.
:param type cls: Class to introspect.
:raise TypeError: If *cls* is not a class.
Matt Harbison
attr: vendor 22.1.0...
r50538 :rtype: bool
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 """
return getattr(cls, "__attrs_attrs__", None) is not None
def assoc(inst, **changes):
"""
Copy *inst* and apply *changes*.
:param inst: Instance of a class with ``attrs`` attributes.
:param changes: Keyword changes in the new copy.
:return: A copy of inst with *changes* incorporated.
:raise attr.exceptions.AttrsAttributeNotFoundError: If *attr_name* couldn't
be found on *cls*.
:raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
class.
.. deprecated:: 17.1.0
Matt Harbison
attr: vendor 22.1.0...
r50538 Use `attrs.evolve` instead if you can.
This function will not be removed du to the slightly different approach
compared to `attrs.evolve`.
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 """
import warnings
Matt Harbison
attr: vendor 22.1.0...
r50538
warnings.warn(
"assoc is deprecated and will be removed after 2018/01.",
DeprecationWarning,
stacklevel=2,
)
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 new = copy.copy(inst)
attrs = fields(inst.__class__)
Matt Harbison
attr: vendor 22.1.0...
r50538 for k, v in changes.items():
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 a = getattr(attrs, k, NOTHING)
if a is NOTHING:
raise AttrsAttributeNotFoundError(
Matt Harbison
attr: vendor 22.1.0...
r50538 "{k} is not an attrs attribute on {cl}.".format(
k=k, cl=new.__class__
)
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 )
_obj_setattr(new, k, v)
return new
def evolve(inst, **changes):
"""
Create a new instance, based on *inst* with *changes* applied.
:param inst: Instance of a class with ``attrs`` attributes.
:param changes: Keyword changes in the new copy.
:return: A copy of inst with *changes* incorporated.
:raise TypeError: If *attr_name* couldn't be found in the class
``__init__``.
:raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
class.
.. versionadded:: 17.1.0
"""
cls = inst.__class__
attrs = fields(cls)
for a in attrs:
if not a.init:
continue
attr_name = a.name # To deal with private attributes.
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)
Matt Harbison
attr: vendor 22.1.0...
r50538
Siddharth Agarwal
thirdparty: vendor attrs...
r34398 return cls(**changes)
Matt Harbison
attr: vendor 22.1.0...
r50538
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