Split cmp into eq and order (#574)
* Split cmp into eq and order Fixes #170 * Fix tests on old pypy3 versions Old as in: currently on AP. * Fix issue number and clarify newsfragment * Clarify behavior and interaction between cmp/eq/order * This sounds better * Address Julian's review comments * Missed a cmp * Test the behavior of Attribute.cmp * Make test more idiomatic * Explain assumptions * Clarify comment * Grammar * One more cmp!
This commit is contained in:
parent
6e7b9f2cfa
commit
08fcfe91d4
|
@ -72,7 +72,7 @@ After *declaring* your attributes ``attrs`` gives you:
|
|||
|
||||
- a concise and explicit overview of the class's attributes,
|
||||
- a nice human-readable ``__repr__``,
|
||||
- a complete set of comparison methods,
|
||||
- a complete set of comparison methods (equality and ordering),
|
||||
- an initializer,
|
||||
- and much more,
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
The ``cmp`` argument to ``attr.s()`` and ``attr.ib()`` is now deprecated.
|
||||
|
||||
Please use ``eq`` to add equality methods (``__eq__`` and ``__ne__``) and ``order`` to add ordering methods (``__lt__``, ``__le__``, ``__gt__``, and ``__ge__``) instead – just like with `dataclasses <https://docs.python.org/3/library/dataclasses.html>`_.
|
||||
|
||||
Both are effectively ``True`` by default but it's enough to set ``eq=False`` to disable both at once.
|
||||
Passing ``eq=False, order=True`` explicitly will raise a ``ValueError`` though.
|
||||
|
||||
Since this is arguably a deeper backward-compatibility break, it will have an extended deprecation period until 2021-06-01.
|
||||
After that day, the ``cmp`` argument will be removed.
|
||||
|
||||
``attr.Attribute`` also isn't orderable anymore.
|
17
conftest.py
17
conftest.py
|
@ -2,8 +2,6 @@ from __future__ import absolute_import, division, print_function
|
|||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from hypothesis import HealthCheck, settings
|
||||
|
||||
|
||||
|
@ -15,21 +13,6 @@ def pytest_configure(config):
|
|||
settings.load_profile("patience")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def C():
|
||||
"""
|
||||
Return a simple but fully featured attrs class with an x and a y attribute.
|
||||
"""
|
||||
import attr
|
||||
|
||||
@attr.s
|
||||
class C(object):
|
||||
x = attr.ib()
|
||||
y = attr.ib()
|
||||
|
||||
return C
|
||||
|
||||
|
||||
collect_ignore = []
|
||||
if sys.version_info[:2] < (3, 6):
|
||||
collect_ignore.extend(
|
||||
|
|
14
docs/api.rst
14
docs/api.rst
|
@ -16,7 +16,7 @@ What follows is the API explanation, if you'd like a more hands-on introduction,
|
|||
Core
|
||||
----
|
||||
|
||||
.. autofunction:: attr.s(these=None, repr_ns=None, repr=True, cmp=True, hash=None, init=True, slots=False, frozen=False, weakref_slot=True, str=False, auto_attribs=False, kw_only=False, cache_hash=False, auto_exc=False)
|
||||
.. autofunction:: attr.s(these=None, repr_ns=None, repr=True, cmp=None, hash=None, init=True, slots=False, frozen=False, weakref_slot=True, str=False, auto_attribs=False, kw_only=False, cache_hash=False, auto_exc=False, eq=None, order=None)
|
||||
|
||||
.. note::
|
||||
|
||||
|
@ -102,7 +102,7 @@ Core
|
|||
... class C(object):
|
||||
... x = attr.ib()
|
||||
>>> attr.fields(C).x
|
||||
Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False)
|
||||
Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, order=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False)
|
||||
|
||||
|
||||
.. autofunction:: attr.make_class
|
||||
|
@ -174,9 +174,9 @@ Helpers
|
|||
... x = attr.ib()
|
||||
... y = attr.ib()
|
||||
>>> attr.fields(C)
|
||||
(Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False))
|
||||
(Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, order=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, order=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False))
|
||||
>>> attr.fields(C)[1]
|
||||
Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False)
|
||||
Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, order=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False)
|
||||
>>> attr.fields(C).y is attr.fields(C)[1]
|
||||
True
|
||||
|
||||
|
@ -191,9 +191,9 @@ Helpers
|
|||
... x = attr.ib()
|
||||
... y = attr.ib()
|
||||
>>> attr.fields_dict(C)
|
||||
{'x': Attribute(name='x', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), 'y': Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False)}
|
||||
{'x': Attribute(name='x', default=NOTHING, validator=None, repr=True, eq=True, order=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), 'y': Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, order=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False)}
|
||||
>>> attr.fields_dict(C)['y']
|
||||
Attribute(name='y', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False)
|
||||
Attribute(name='y', default=NOTHING, validator=None, repr=True, eq=True, order=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False)
|
||||
>>> attr.fields_dict(C)['y'] is attr.fields(C).y
|
||||
True
|
||||
|
||||
|
@ -288,7 +288,7 @@ See :func:`asdict` for examples.
|
|||
>>> attr.validate(i)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: ("'x' must be <type 'int'> (got '1' that is a <type 'str'>).", Attribute(name='x', default=NOTHING, validator=<instance_of validator for type <type 'int'>>, repr=True, cmp=True, hash=None, init=True, type=None, kw_only=False), <type 'int'>, '1')
|
||||
TypeError: ("'x' must be <class 'int'> (got '1' that is a <class 'str'>).", ...)
|
||||
|
||||
|
||||
Validators can be globally disabled if you want to run them only in development and tests but not in production because you fear their performance impact:
|
||||
|
|
|
@ -534,8 +534,6 @@ The generated ``__init__`` method will have an attribute called ``__annotations_
|
|||
However it's useful for writing your own validators or serialization frameworks.
|
||||
|
||||
|
||||
.. _slots:
|
||||
|
||||
Slots
|
||||
-----
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ So it is fairly simple to build your own decorators on top of ``attrs``:
|
|||
... @attr.s
|
||||
... class C(object):
|
||||
... a = attr.ib()
|
||||
(Attribute(name='a', default=NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False),)
|
||||
(Attribute(name='a', default=NOTHING, validator=None, repr=True, eq=True, order=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False),)
|
||||
|
||||
|
||||
.. warning::
|
||||
|
@ -99,10 +99,18 @@ Here are some tips for effective use of metadata:
|
|||
|
||||
>>> MY_TYPE_METADATA = '__my_type_metadata'
|
||||
>>>
|
||||
>>> def typed(cls, default=attr.NOTHING, validator=None, repr=True, cmp=True, hash=None, init=True, metadata={}, type=None, converter=None):
|
||||
>>> def typed(
|
||||
... cls, default=attr.NOTHING, validator=None, repr=True,
|
||||
... eq=True, order=None, hash=None, init=True, metadata={},
|
||||
... type=None, converter=None
|
||||
... ):
|
||||
... metadata = dict() if not metadata else metadata
|
||||
... metadata[MY_TYPE_METADATA] = cls
|
||||
... return attr.ib(default, validator, repr, cmp, hash, init, metadata, type, converter)
|
||||
... return attr.ib(
|
||||
... default=default, validator=validator, repr=repr,
|
||||
... eq=eq, order=order, hash=hash, init=init,
|
||||
... metadata=metadata, type=type, converter=converter
|
||||
... )
|
||||
>>>
|
||||
>>> @attr.s
|
||||
... class C(object):
|
||||
|
|
|
@ -10,7 +10,7 @@ Hash Method Generation
|
|||
Leave it at ``None`` which means that ``attrs`` will do the right thing for you, depending on the other parameters:
|
||||
|
||||
- If you want to make objects hashable by value: use ``@attr.s(frozen=True)``.
|
||||
- If you want hashing and comparison by object identity: use ``@attr.s(cmp=False)``
|
||||
- If you want hashing and equality by object identity: use ``@attr.s(eq=False)``
|
||||
|
||||
Setting ``hash`` yourself can have unexpected consequences so we recommend to tinker with it only if you know exactly what you're doing.
|
||||
|
||||
|
@ -34,13 +34,13 @@ Because according to the definition_ from the official Python docs, the returned
|
|||
Python 2 on the other hand will happily let you shoot your foot off.
|
||||
Unfortunately ``attrs`` currently mimics Python 2's behavior for backward compatibility reasons if you set ``hash=False``.
|
||||
|
||||
The *correct way* to achieve hashing by id is to set ``@attr.s(cmp=False)``.
|
||||
Setting ``@attr.s(hash=False)`` (that implies ``cmp=True``) is almost certainly a *bug*.
|
||||
The *correct way* to achieve hashing by id is to set ``@attr.s(eq=False)``.
|
||||
Setting ``@attr.s(hash=False)`` (which implies ``eq=True``) is almost certainly a *bug*.
|
||||
|
||||
.. warning::
|
||||
|
||||
Be careful when subclassing!
|
||||
Setting ``cmp=False`` on class whose base class has a non-default ``__hash__`` method, will *not* make ``attrs`` remove that ``__hash__`` for you.
|
||||
Setting ``eq=False`` on a class whose base class has a non-default ``__hash__`` method will *not* make ``attrs`` remove that ``__hash__`` for you.
|
||||
|
||||
It is part of ``attrs``'s philosophy to only *add* to classes so you have the freedom to customize your classes as you wish.
|
||||
So if you want to *get rid* of methods, you'll have to do it by hand.
|
||||
|
@ -65,8 +65,9 @@ For a more thorough explanation of this topic, please refer to this blog post: `
|
|||
|
||||
Hashing and Mutability
|
||||
----------------------
|
||||
|
||||
Changing any field involved in hash code computation after the first call to ``__hash__`` (typically this would be after its insertion into a hash-based collection) can result in silent bugs.
|
||||
Therefore, it is strongly recommended that hashable classes be ``frozen.``
|
||||
Therefore, it is strongly recommended that hashable classes be ``frozen``.
|
||||
Beware, however, that this is not a complete guarantee of safety:
|
||||
if a field points to an object and that object is mutated, the hash code may change, but ``frozen`` will not protect you.
|
||||
|
||||
|
|
|
@ -53,16 +53,14 @@ class Attribute(Generic[_T]):
|
|||
validator: Optional[_ValidatorType[_T]]
|
||||
repr: _ReprArgType
|
||||
cmp: bool
|
||||
eq: bool
|
||||
order: bool
|
||||
hash: Optional[bool]
|
||||
init: bool
|
||||
converter: Optional[_ConverterType[_T]]
|
||||
metadata: Dict[Any, Any]
|
||||
type: Optional[Type[_T]]
|
||||
kw_only: bool
|
||||
def __lt__(self, x: Attribute[_T]) -> bool: ...
|
||||
def __le__(self, x: Attribute[_T]) -> bool: ...
|
||||
def __gt__(self, x: Attribute[_T]) -> bool: ...
|
||||
def __ge__(self, x: Attribute[_T]) -> bool: ...
|
||||
|
||||
# NOTE: We had several choices for the annotation to use for type arg:
|
||||
# 1) Type[_T]
|
||||
|
@ -92,7 +90,7 @@ def attrib(
|
|||
default: None = ...,
|
||||
validator: None = ...,
|
||||
repr: _ReprArgType = ...,
|
||||
cmp: bool = ...,
|
||||
cmp: Optional[bool] = ...,
|
||||
hash: Optional[bool] = ...,
|
||||
init: bool = ...,
|
||||
metadata: Optional[Mapping[Any, Any]] = ...,
|
||||
|
@ -100,6 +98,8 @@ def attrib(
|
|||
converter: None = ...,
|
||||
factory: None = ...,
|
||||
kw_only: bool = ...,
|
||||
eq: Optional[bool] = ...,
|
||||
order: Optional[bool] = ...,
|
||||
) -> Any: ...
|
||||
|
||||
# This form catches an explicit None or no default and infers the type from the other arguments.
|
||||
|
@ -108,7 +108,7 @@ def attrib(
|
|||
default: None = ...,
|
||||
validator: Optional[_ValidatorArgType[_T]] = ...,
|
||||
repr: _ReprArgType = ...,
|
||||
cmp: bool = ...,
|
||||
cmp: Optional[bool] = ...,
|
||||
hash: Optional[bool] = ...,
|
||||
init: bool = ...,
|
||||
metadata: Optional[Mapping[Any, Any]] = ...,
|
||||
|
@ -116,6 +116,8 @@ def attrib(
|
|||
converter: Optional[_ConverterType[_T]] = ...,
|
||||
factory: Optional[Callable[[], _T]] = ...,
|
||||
kw_only: bool = ...,
|
||||
eq: Optional[bool] = ...,
|
||||
order: Optional[bool] = ...,
|
||||
) -> _T: ...
|
||||
|
||||
# This form catches an explicit default argument.
|
||||
|
@ -124,7 +126,7 @@ def attrib(
|
|||
default: _T,
|
||||
validator: Optional[_ValidatorArgType[_T]] = ...,
|
||||
repr: _ReprArgType = ...,
|
||||
cmp: bool = ...,
|
||||
cmp: Optional[bool] = ...,
|
||||
hash: Optional[bool] = ...,
|
||||
init: bool = ...,
|
||||
metadata: Optional[Mapping[Any, Any]] = ...,
|
||||
|
@ -132,6 +134,8 @@ def attrib(
|
|||
converter: Optional[_ConverterType[_T]] = ...,
|
||||
factory: Optional[Callable[[], _T]] = ...,
|
||||
kw_only: bool = ...,
|
||||
eq: Optional[bool] = ...,
|
||||
order: Optional[bool] = ...,
|
||||
) -> _T: ...
|
||||
|
||||
# This form covers type=non-Type: e.g. forward references (str), Any
|
||||
|
@ -140,7 +144,7 @@ def attrib(
|
|||
default: Optional[_T] = ...,
|
||||
validator: Optional[_ValidatorArgType[_T]] = ...,
|
||||
repr: _ReprArgType = ...,
|
||||
cmp: bool = ...,
|
||||
cmp: Optional[bool] = ...,
|
||||
hash: Optional[bool] = ...,
|
||||
init: bool = ...,
|
||||
metadata: Optional[Mapping[Any, Any]] = ...,
|
||||
|
@ -148,6 +152,8 @@ def attrib(
|
|||
converter: Optional[_ConverterType[_T]] = ...,
|
||||
factory: Optional[Callable[[], _T]] = ...,
|
||||
kw_only: bool = ...,
|
||||
eq: Optional[bool] = ...,
|
||||
order: Optional[bool] = ...,
|
||||
) -> Any: ...
|
||||
@overload
|
||||
def attrs(
|
||||
|
@ -155,7 +161,7 @@ def attrs(
|
|||
these: Optional[Dict[str, Any]] = ...,
|
||||
repr_ns: Optional[str] = ...,
|
||||
repr: bool = ...,
|
||||
cmp: bool = ...,
|
||||
cmp: Optional[bool] = ...,
|
||||
hash: Optional[bool] = ...,
|
||||
init: bool = ...,
|
||||
slots: bool = ...,
|
||||
|
@ -166,6 +172,8 @@ def attrs(
|
|||
kw_only: bool = ...,
|
||||
cache_hash: bool = ...,
|
||||
auto_exc: bool = ...,
|
||||
eq: Optional[bool] = ...,
|
||||
order: Optional[bool] = ...,
|
||||
) -> _C: ...
|
||||
@overload
|
||||
def attrs(
|
||||
|
@ -173,7 +181,7 @@ def attrs(
|
|||
these: Optional[Dict[str, Any]] = ...,
|
||||
repr_ns: Optional[str] = ...,
|
||||
repr: bool = ...,
|
||||
cmp: bool = ...,
|
||||
cmp: Optional[bool] = ...,
|
||||
hash: Optional[bool] = ...,
|
||||
init: bool = ...,
|
||||
slots: bool = ...,
|
||||
|
@ -184,6 +192,8 @@ def attrs(
|
|||
kw_only: bool = ...,
|
||||
cache_hash: bool = ...,
|
||||
auto_exc: bool = ...,
|
||||
eq: Optional[bool] = ...,
|
||||
order: Optional[bool] = ...,
|
||||
) -> Callable[[_C], _C]: ...
|
||||
|
||||
# TODO: add support for returning NamedTuple from the mypy plugin
|
||||
|
@ -202,7 +212,7 @@ def make_class(
|
|||
bases: Tuple[type, ...] = ...,
|
||||
repr_ns: Optional[str] = ...,
|
||||
repr: bool = ...,
|
||||
cmp: bool = ...,
|
||||
cmp: Optional[bool] = ...,
|
||||
hash: Optional[bool] = ...,
|
||||
init: bool = ...,
|
||||
slots: bool = ...,
|
||||
|
@ -213,6 +223,8 @@ def make_class(
|
|||
kw_only: bool = ...,
|
||||
cache_hash: bool = ...,
|
||||
auto_exc: bool = ...,
|
||||
eq: Optional[bool] = ...,
|
||||
order: Optional[bool] = ...,
|
||||
) -> type: ...
|
||||
|
||||
# _funcs --
|
||||
|
|
|
@ -5,6 +5,7 @@ import linecache
|
|||
import sys
|
||||
import threading
|
||||
import uuid
|
||||
import warnings
|
||||
|
||||
from operator import itemgetter
|
||||
|
||||
|
@ -73,7 +74,7 @@ def attrib(
|
|||
default=NOTHING,
|
||||
validator=None,
|
||||
repr=True,
|
||||
cmp=True,
|
||||
cmp=None,
|
||||
hash=None,
|
||||
init=True,
|
||||
metadata=None,
|
||||
|
@ -81,6 +82,8 @@ def attrib(
|
|||
converter=None,
|
||||
factory=None,
|
||||
kw_only=False,
|
||||
eq=None,
|
||||
order=None,
|
||||
):
|
||||
"""
|
||||
Create a new attribute on a class.
|
||||
|
@ -135,10 +138,15 @@ def attrib(
|
|||
as-is, i.e. it will be used directly *instead* of calling ``repr()``
|
||||
(the default).
|
||||
:type repr: a ``bool`` or a ``callable`` to use a custom function.
|
||||
:param bool cmp: Include this attribute in the generated comparison methods
|
||||
(``__eq__`` et al).
|
||||
:param bool eq: If ``True`` (default), include this attribute in the
|
||||
generated ``__eq__`` and ``__ne__`` methods that check two instances
|
||||
for equality.
|
||||
:param bool order: If ``True`` (default), include this attributes in the
|
||||
generated ``__lt__``, ``__le__``, ``__gt__`` and ``__ge__`` methods.
|
||||
:param bool cmp: Setting to ``True`` is equivalent to setting ``eq=True,
|
||||
order=True``. Deprecated in favor of *eq* and *order*.
|
||||
:param hash: Include this attribute in the generated ``__hash__``
|
||||
method. If ``None`` (default), mirror *cmp*'s value. This is the
|
||||
method. If ``None`` (default), mirror *eq*'s value. This is the
|
||||
correct behavior according the Python spec. Setting this value to
|
||||
anything else than ``None`` is *discouraged*.
|
||||
:type hash: ``bool`` or ``None``
|
||||
|
@ -171,7 +179,7 @@ def attrib(
|
|||
.. versionadded:: 16.3.0 *metadata*
|
||||
.. versionchanged:: 17.1.0 *validator* can be a ``list`` now.
|
||||
.. versionchanged:: 17.1.0
|
||||
*hash* is ``None`` and therefore mirrors *cmp* by default.
|
||||
*hash* is ``None`` and therefore mirrors *eq* by default.
|
||||
.. versionadded:: 17.3.0 *type*
|
||||
.. deprecated:: 17.4.0 *convert*
|
||||
.. versionadded:: 17.4.0 *converter* as a replacement for the deprecated
|
||||
|
@ -181,7 +189,11 @@ def attrib(
|
|||
.. versionadded:: 18.2.0 *kw_only*
|
||||
.. versionchanged:: 19.2.0 *convert* keyword argument removed
|
||||
.. versionchanged:: 19.2.0 *repr* also accepts a custom callable.
|
||||
.. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.
|
||||
.. versionadded:: 19.2.0 *eq* and *order*
|
||||
"""
|
||||
eq, order = _determine_eq_order(cmp, eq, order)
|
||||
|
||||
if hash is not None and hash is not True and hash is not False:
|
||||
raise TypeError(
|
||||
"Invalid value for hash. Must be True, False, or None."
|
||||
|
@ -204,13 +216,15 @@ def attrib(
|
|||
default=default,
|
||||
validator=validator,
|
||||
repr=repr,
|
||||
cmp=cmp,
|
||||
cmp=None,
|
||||
hash=hash,
|
||||
init=init,
|
||||
converter=converter,
|
||||
metadata=metadata,
|
||||
type=type,
|
||||
kw_only=kw_only,
|
||||
eq=eq,
|
||||
order=order,
|
||||
)
|
||||
|
||||
|
||||
|
@ -678,14 +692,22 @@ class _ClassBuilder(object):
|
|||
|
||||
return self
|
||||
|
||||
def add_cmp(self):
|
||||
def add_eq(self):
|
||||
cd = self._cls_dict
|
||||
|
||||
cd["__eq__"], cd["__ne__"], cd["__lt__"], cd["__le__"], cd[
|
||||
"__gt__"
|
||||
], cd["__ge__"] = (
|
||||
cd["__eq__"], cd["__ne__"] = (
|
||||
self._add_method_dunders(meth)
|
||||
for meth in _make_cmp(self._cls, self._attrs)
|
||||
for meth in _make_eq(self._cls, self._attrs)
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
def add_order(self):
|
||||
cd = self._cls_dict
|
||||
|
||||
cd["__lt__"], cd["__le__"], cd["__gt__"], cd["__ge__"] = (
|
||||
self._add_method_dunders(meth)
|
||||
for meth in _make_order(self._cls, self._attrs)
|
||||
)
|
||||
|
||||
return self
|
||||
|
@ -709,12 +731,45 @@ class _ClassBuilder(object):
|
|||
return method
|
||||
|
||||
|
||||
_CMP_DEPRECATION = (
|
||||
"The usage of `cmp` is deprecated and will be removed on or after "
|
||||
"2021-06-01. Please use `eq` and `order` instead."
|
||||
)
|
||||
|
||||
|
||||
def _determine_eq_order(cmp, eq, order):
|
||||
"""
|
||||
Validate the combination of *cmp*, *eq*, and *order*. Derive the effective
|
||||
values of eq and order.
|
||||
"""
|
||||
if cmp is not None and any((eq is not None, order is not None)):
|
||||
raise ValueError("Don't mix `cmp` with `eq' and `order`.")
|
||||
|
||||
# cmp takes precedence due to bw-compatibility.
|
||||
if cmp is not None:
|
||||
warnings.warn(_CMP_DEPRECATION, DeprecationWarning, stacklevel=3)
|
||||
|
||||
return cmp, cmp
|
||||
|
||||
# If left None, equality is on and ordering mirrors equality.
|
||||
if eq is None:
|
||||
eq = True
|
||||
|
||||
if order is None:
|
||||
order = eq
|
||||
|
||||
if eq is False and order is True:
|
||||
raise ValueError("`order` can only be True if `eq` is True too.")
|
||||
|
||||
return eq, order
|
||||
|
||||
|
||||
def attrs(
|
||||
maybe_cls=None,
|
||||
these=None,
|
||||
repr_ns=None,
|
||||
repr=True,
|
||||
cmp=True,
|
||||
cmp=None,
|
||||
hash=None,
|
||||
init=True,
|
||||
slots=False,
|
||||
|
@ -725,6 +780,8 @@ def attrs(
|
|||
kw_only=False,
|
||||
cache_hash=False,
|
||||
auto_exc=False,
|
||||
eq=None,
|
||||
order=None,
|
||||
):
|
||||
r"""
|
||||
A class decorator that adds `dunder
|
||||
|
@ -754,17 +811,28 @@ def attrs(
|
|||
:param bool str: Create a ``__str__`` method that is identical to
|
||||
``__repr__``. This is usually not necessary except for
|
||||
`Exception`\ s.
|
||||
:param bool cmp: Create ``__eq__``, ``__ne__``, ``__lt__``, ``__le__``,
|
||||
``__gt__``, and ``__ge__`` methods that compare the class as if it were
|
||||
a tuple of its ``attrs`` attributes. But the attributes are *only*
|
||||
compared, if the types of both classes are *identical*!
|
||||
:param bool eq: If ``True`` or ``None`` (default), add ``__eq__`` and
|
||||
``__ne__`` methods that check two instances for equality.
|
||||
|
||||
They compare the instances as if they were tuples of their ``attrs``
|
||||
attributes, but only iff the types of both classes are *identical*!
|
||||
:type eq: `bool` or `None`
|
||||
:param bool order: If ``True``, add ``__lt__``, ``__le__``, ``__gt__``,
|
||||
and ``__ge__`` methods that behave like *eq* above and allow instances
|
||||
to be ordered. If ``None`` (default) mirror value of *eq*.
|
||||
:type order: `bool` or `None`
|
||||
:param cmp: Setting to ``True`` is equivalent to setting ``eq=True,
|
||||
order=True``. Deprecated in favor of *eq* and *order*, has precedence
|
||||
over them for backward-compatibility though. Must not be mixed with
|
||||
*eq* or *order*.
|
||||
:type cmp: `bool` or `None`
|
||||
:param hash: If ``None`` (default), the ``__hash__`` method is generated
|
||||
according how *cmp* and *frozen* are set.
|
||||
according how *eq* and *frozen* are set.
|
||||
|
||||
1. If *both* are True, ``attrs`` will generate a ``__hash__`` for you.
|
||||
2. If *cmp* is True and *frozen* is False, ``__hash__`` will be set to
|
||||
2. If *eq* is True and *frozen* is False, ``__hash__`` will be set to
|
||||
None, marking it unhashable (which it is).
|
||||
3. If *cmp* is False, ``__hash__`` will be left untouched meaning the
|
||||
3. If *eq* is False, ``__hash__`` will be left untouched meaning the
|
||||
``__hash__`` method of the base class will be used (if base class is
|
||||
``object``, this means it will fall back to id-based hashing.).
|
||||
|
||||
|
@ -838,10 +906,10 @@ def attrs(
|
|||
following happens to behave like a well-behaved Python exceptions
|
||||
class:
|
||||
|
||||
- the values for *cmp* and *hash* are ignored and the instances compare
|
||||
and hash by the instance's ids (N.B. ``attrs`` will *not* remove
|
||||
existing implementations of ``__hash__`` or the equality methods. It
|
||||
just won't add own ones.),
|
||||
- the values for *eq*, *order*, and *hash* are ignored and the
|
||||
instances compare and hash by the instance's ids (N.B. ``attrs`` will
|
||||
*not* remove existing implementations of ``__hash__`` or the equality
|
||||
methods. It just won't add own ones.),
|
||||
- all attributes that are either passed into ``__init__`` or have a
|
||||
default value are additionally available as a tuple in the ``args``
|
||||
attribute,
|
||||
|
@ -869,7 +937,10 @@ def attrs(
|
|||
.. versionadded:: 18.2.0 *kw_only*
|
||||
.. versionadded:: 18.2.0 *cache_hash*
|
||||
.. versionadded:: 19.1.0 *auto_exc*
|
||||
.. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.
|
||||
.. versionadded:: 19.2.0 *eq* and *order*
|
||||
"""
|
||||
eq, order = _determine_eq_order(cmp, eq, order)
|
||||
|
||||
def wrap(cls):
|
||||
|
||||
|
@ -894,15 +965,17 @@ def attrs(
|
|||
builder.add_repr(repr_ns)
|
||||
if str is True:
|
||||
builder.add_str()
|
||||
if cmp is True and not is_exc:
|
||||
builder.add_cmp()
|
||||
if eq is True and not is_exc:
|
||||
builder.add_eq()
|
||||
if order is True and not is_exc:
|
||||
builder.add_order()
|
||||
|
||||
if hash is not True and hash is not False and hash is not None:
|
||||
# Can't use `hash in` because 1 == True for example.
|
||||
raise TypeError(
|
||||
"Invalid value for hash. Must be True, False, or None."
|
||||
)
|
||||
elif hash is False or (hash is None and cmp is False) or is_exc:
|
||||
elif hash is False or (hash is None and eq is False) or is_exc:
|
||||
# Don't do anything. Should fall back to __object__'s __hash__
|
||||
# which is by id.
|
||||
if cache_hash:
|
||||
|
@ -911,7 +984,7 @@ def attrs(
|
|||
" hashing must be either explicitly or implicitly "
|
||||
"enabled."
|
||||
)
|
||||
elif hash is True or (hash is None and cmp is True and frozen is True):
|
||||
elif hash is True or (hash is None and eq is True and frozen is True):
|
||||
# Build a __hash__ if told so, or if it's safe.
|
||||
builder.add_hash()
|
||||
else:
|
||||
|
@ -1013,9 +1086,7 @@ def _generate_unique_filename(cls, func_name):
|
|||
|
||||
def _make_hash(cls, attrs, frozen, cache_hash):
|
||||
attrs = tuple(
|
||||
a
|
||||
for a in attrs
|
||||
if a.hash is True or (a.hash is None and a.cmp is True)
|
||||
a for a in attrs if a.hash is True or (a.hash is None and a.eq is True)
|
||||
)
|
||||
|
||||
tab = " "
|
||||
|
@ -1093,8 +1164,8 @@ def __ne__(self, other):
|
|||
return not result
|
||||
|
||||
|
||||
def _make_cmp(cls, attrs):
|
||||
attrs = [a for a in attrs if a.cmp]
|
||||
def _make_eq(cls, attrs):
|
||||
attrs = [a for a in attrs if a.eq]
|
||||
|
||||
unique_filename = _generate_unique_filename(cls, "eq")
|
||||
lines = [
|
||||
|
@ -1129,8 +1200,11 @@ def _make_cmp(cls, attrs):
|
|||
script.splitlines(True),
|
||||
unique_filename,
|
||||
)
|
||||
eq = locs["__eq__"]
|
||||
ne = __ne__
|
||||
return locs["__eq__"], __ne__
|
||||
|
||||
|
||||
def _make_order(cls, attrs):
|
||||
attrs = [a for a in attrs if a.order]
|
||||
|
||||
def attrs_to_tuple(obj):
|
||||
"""
|
||||
|
@ -1174,19 +1248,17 @@ def _make_cmp(cls, attrs):
|
|||
|
||||
return NotImplemented
|
||||
|
||||
return eq, ne, __lt__, __le__, __gt__, __ge__
|
||||
return __lt__, __le__, __gt__, __ge__
|
||||
|
||||
|
||||
def _add_cmp(cls, attrs=None):
|
||||
def _add_eq(cls, attrs=None):
|
||||
"""
|
||||
Add comparison methods to *cls*.
|
||||
Add equality methods to *cls* with *attrs*.
|
||||
"""
|
||||
if attrs is None:
|
||||
attrs = cls.__attrs_attrs__
|
||||
|
||||
cls.__eq__, cls.__ne__, cls.__lt__, cls.__le__, cls.__gt__, cls.__ge__ = _make_cmp( # noqa
|
||||
cls, attrs
|
||||
)
|
||||
cls.__eq__, cls.__ne__ = _make_eq(cls, attrs)
|
||||
|
||||
return cls
|
||||
|
||||
|
@ -1682,7 +1754,8 @@ class Attribute(object):
|
|||
"default",
|
||||
"validator",
|
||||
"repr",
|
||||
"cmp",
|
||||
"eq",
|
||||
"order",
|
||||
"hash",
|
||||
"init",
|
||||
"metadata",
|
||||
|
@ -1697,14 +1770,18 @@ class Attribute(object):
|
|||
default,
|
||||
validator,
|
||||
repr,
|
||||
cmp,
|
||||
cmp, # XXX: unused, remove along with other cmp code.
|
||||
hash,
|
||||
init,
|
||||
metadata=None,
|
||||
type=None,
|
||||
converter=None,
|
||||
kw_only=False,
|
||||
eq=None,
|
||||
order=None,
|
||||
):
|
||||
eq, order = _determine_eq_order(cmp, eq, order)
|
||||
|
||||
# Cache this descriptor here to speed things up later.
|
||||
bound_setattr = _obj_setattr.__get__(self, Attribute)
|
||||
|
||||
|
@ -1714,7 +1791,8 @@ class Attribute(object):
|
|||
bound_setattr("default", default)
|
||||
bound_setattr("validator", validator)
|
||||
bound_setattr("repr", repr)
|
||||
bound_setattr("cmp", cmp)
|
||||
bound_setattr("eq", eq)
|
||||
bound_setattr("order", order)
|
||||
bound_setattr("hash", hash)
|
||||
bound_setattr("init", init)
|
||||
bound_setattr("converter", converter)
|
||||
|
@ -1757,9 +1835,19 @@ class Attribute(object):
|
|||
validator=ca._validator,
|
||||
default=ca._default,
|
||||
type=type,
|
||||
cmp=None,
|
||||
**inst_dict
|
||||
)
|
||||
|
||||
@property
|
||||
def cmp(self):
|
||||
"""
|
||||
Simulate the presence of a cmp attribute and warn.
|
||||
"""
|
||||
warnings.warn(_CMP_DEPRECATION, DeprecationWarning, stacklevel=2)
|
||||
|
||||
return self.eq and self.order
|
||||
|
||||
# Don't use attr.assoc since fields(Attribute) doesn't work
|
||||
def _assoc(self, **changes):
|
||||
"""
|
||||
|
@ -1807,7 +1895,9 @@ _a = [
|
|||
default=NOTHING,
|
||||
validator=None,
|
||||
repr=True,
|
||||
cmp=True,
|
||||
cmp=None,
|
||||
eq=True,
|
||||
order=False,
|
||||
hash=(name != "metadata"),
|
||||
init=True,
|
||||
)
|
||||
|
@ -1815,7 +1905,7 @@ _a = [
|
|||
]
|
||||
|
||||
Attribute = _add_hash(
|
||||
_add_cmp(_add_repr(Attribute, attrs=_a), attrs=_a),
|
||||
_add_eq(_add_repr(Attribute, attrs=_a), attrs=_a),
|
||||
attrs=[a for a in _a if a.hash],
|
||||
)
|
||||
|
||||
|
@ -1833,7 +1923,8 @@ class _CountingAttr(object):
|
|||
"counter",
|
||||
"_default",
|
||||
"repr",
|
||||
"cmp",
|
||||
"eq",
|
||||
"order",
|
||||
"hash",
|
||||
"init",
|
||||
"metadata",
|
||||
|
@ -1848,22 +1939,34 @@ class _CountingAttr(object):
|
|||
default=NOTHING,
|
||||
validator=None,
|
||||
repr=True,
|
||||
cmp=True,
|
||||
cmp=None,
|
||||
hash=True,
|
||||
init=True,
|
||||
kw_only=False,
|
||||
eq=True,
|
||||
order=False,
|
||||
)
|
||||
for name in (
|
||||
"counter",
|
||||
"_default",
|
||||
"repr",
|
||||
"eq",
|
||||
"order",
|
||||
"hash",
|
||||
"init",
|
||||
)
|
||||
for name in ("counter", "_default", "repr", "cmp", "hash", "init")
|
||||
) + (
|
||||
Attribute(
|
||||
name="metadata",
|
||||
default=None,
|
||||
validator=None,
|
||||
repr=True,
|
||||
cmp=True,
|
||||
cmp=None,
|
||||
hash=False,
|
||||
init=True,
|
||||
kw_only=False,
|
||||
eq=True,
|
||||
order=False,
|
||||
),
|
||||
)
|
||||
cls_counter = 0
|
||||
|
@ -1873,13 +1976,15 @@ class _CountingAttr(object):
|
|||
default,
|
||||
validator,
|
||||
repr,
|
||||
cmp,
|
||||
cmp, # XXX: unused, remove along with cmp
|
||||
hash,
|
||||
init,
|
||||
converter,
|
||||
metadata,
|
||||
type,
|
||||
kw_only,
|
||||
eq,
|
||||
order,
|
||||
):
|
||||
_CountingAttr.cls_counter += 1
|
||||
self.counter = _CountingAttr.cls_counter
|
||||
|
@ -1890,7 +1995,8 @@ class _CountingAttr(object):
|
|||
else:
|
||||
self._validator = validator
|
||||
self.repr = repr
|
||||
self.cmp = cmp
|
||||
self.eq = eq
|
||||
self.order = order
|
||||
self.hash = hash
|
||||
self.init = init
|
||||
self.converter = converter
|
||||
|
@ -1930,7 +2036,7 @@ class _CountingAttr(object):
|
|||
return meth
|
||||
|
||||
|
||||
_CountingAttr = _add_cmp(_add_repr(_CountingAttr))
|
||||
_CountingAttr = _add_eq(_add_repr(_CountingAttr))
|
||||
|
||||
|
||||
@attrs(slots=True, init=False, hash=True)
|
||||
|
@ -2011,6 +2117,14 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments):
|
|||
except (AttributeError, ValueError):
|
||||
pass
|
||||
|
||||
# We do it here for proper warnings with meaningful stacklevel.
|
||||
cmp = attributes_arguments.pop("cmp", None)
|
||||
attributes_arguments["eq"], attributes_arguments[
|
||||
"order"
|
||||
] = _determine_eq_order(
|
||||
cmp, attributes_arguments.get("eq"), attributes_arguments.get("order")
|
||||
)
|
||||
|
||||
return _attrs(these=cls_dict, **attributes_arguments)(type_)
|
||||
|
||||
|
||||
|
|
|
@ -14,6 +14,9 @@ import attr
|
|||
from .utils import make_class
|
||||
|
||||
|
||||
optional_bool = st.one_of(st.none(), st.booleans())
|
||||
|
||||
|
||||
def gen_attr_names():
|
||||
"""
|
||||
Generate names for attributes, 'a'...'z', then 'aa'...'zz'.
|
||||
|
@ -131,7 +134,8 @@ def simple_attrs_with_metadata(draw):
|
|||
default=c_attr._default,
|
||||
validator=c_attr._validator,
|
||||
repr=c_attr.repr,
|
||||
cmp=c_attr.cmp,
|
||||
eq=c_attr.eq,
|
||||
order=c_attr.order,
|
||||
hash=c_attr.hash,
|
||||
init=c_attr.init,
|
||||
metadata=metadata,
|
||||
|
|
|
@ -11,15 +11,17 @@ from copy import deepcopy
|
|||
import pytest
|
||||
import six
|
||||
|
||||
from hypothesis import given
|
||||
from hypothesis import assume, given
|
||||
from hypothesis.strategies import booleans
|
||||
|
||||
import attr
|
||||
|
||||
from attr._compat import TYPE
|
||||
from attr._compat import PY2, TYPE
|
||||
from attr._make import NOTHING, Attribute
|
||||
from attr.exceptions import FrozenInstanceError
|
||||
|
||||
from .strategies import optional_bool
|
||||
|
||||
|
||||
@attr.s
|
||||
class C1(object):
|
||||
|
@ -124,7 +126,9 @@ class TestDarkMagic(object):
|
|||
default=foo,
|
||||
validator=None,
|
||||
repr=True,
|
||||
cmp=True,
|
||||
cmp=None,
|
||||
eq=True,
|
||||
order=True,
|
||||
hash=None,
|
||||
init=True,
|
||||
),
|
||||
|
@ -133,7 +137,9 @@ class TestDarkMagic(object):
|
|||
default=attr.Factory(list),
|
||||
validator=None,
|
||||
repr=True,
|
||||
cmp=True,
|
||||
cmp=None,
|
||||
eq=True,
|
||||
order=True,
|
||||
hash=None,
|
||||
init=True,
|
||||
),
|
||||
|
@ -181,13 +187,16 @@ class TestDarkMagic(object):
|
|||
`attr.make_class` works.
|
||||
"""
|
||||
PC = attr.make_class("PC", ["a", "b"], slots=slots, frozen=frozen)
|
||||
|
||||
assert (
|
||||
Attribute(
|
||||
name="a",
|
||||
default=NOTHING,
|
||||
validator=None,
|
||||
repr=True,
|
||||
cmp=True,
|
||||
cmp=None,
|
||||
eq=True,
|
||||
order=True,
|
||||
hash=None,
|
||||
init=True,
|
||||
),
|
||||
|
@ -196,7 +205,9 @@ class TestDarkMagic(object):
|
|||
default=NOTHING,
|
||||
validator=None,
|
||||
repr=True,
|
||||
cmp=True,
|
||||
cmp=None,
|
||||
eq=True,
|
||||
order=True,
|
||||
hash=None,
|
||||
init=True,
|
||||
),
|
||||
|
@ -380,7 +391,7 @@ class TestDarkMagic(object):
|
|||
HashByIDBackwardCompat(1)
|
||||
)
|
||||
|
||||
@attr.s(hash=False, cmp=False)
|
||||
@attr.s(hash=False, eq=False)
|
||||
class HashByID(object):
|
||||
x = attr.ib()
|
||||
|
||||
|
@ -412,10 +423,10 @@ class TestDarkMagic(object):
|
|||
@pytest.mark.parametrize("slots", [True, False])
|
||||
def test_hash_false_cmp_false(self, slots):
|
||||
"""
|
||||
hash=False and cmp=False make a class hashable by ID.
|
||||
hash=False and eq=False make a class hashable by ID.
|
||||
"""
|
||||
|
||||
@attr.s(hash=False, cmp=False, slots=slots)
|
||||
@attr.s(hash=False, eq=False, slots=slots)
|
||||
class C(object):
|
||||
pass
|
||||
|
||||
|
@ -581,3 +592,76 @@ class TestDarkMagic(object):
|
|||
x = attr.ib()
|
||||
|
||||
FooError(1)
|
||||
|
||||
@pytest.mark.parametrize("slots", [True, False])
|
||||
@pytest.mark.parametrize("frozen", [True, False])
|
||||
def test_eq_only(self, slots, frozen):
|
||||
"""
|
||||
Classes with order=False cannot be ordered.
|
||||
|
||||
Python 3 throws a TypeError, in Python2 we have to check for the
|
||||
absence.
|
||||
"""
|
||||
|
||||
@attr.s(eq=True, order=False, slots=slots, frozen=frozen)
|
||||
class C(object):
|
||||
x = attr.ib()
|
||||
|
||||
if not PY2:
|
||||
possible_errors = (
|
||||
"unorderable types: C() < C()",
|
||||
"'<' not supported between instances of 'C' and 'C'",
|
||||
"unorderable types: C < C", # old PyPy 3
|
||||
)
|
||||
|
||||
with pytest.raises(TypeError) as ei:
|
||||
C(5) < C(6)
|
||||
|
||||
assert ei.value.args[0] in possible_errors
|
||||
else:
|
||||
i = C(42)
|
||||
for m in ("lt", "le", "gt", "ge"):
|
||||
assert None is getattr(i, "__%s__" % (m,), None)
|
||||
|
||||
@given(cmp=optional_bool, eq=optional_bool, order=optional_bool)
|
||||
def test_cmp_deprecated_attribute(self, cmp, eq, order):
|
||||
"""
|
||||
Accessing Attribute.cmp raises a deprecation warning but returns True
|
||||
if cmp is True, or eq and order are *both* effectively True.
|
||||
"""
|
||||
# These cases are invalid and raise a ValueError.
|
||||
assume(cmp is None or (eq is None and order is None))
|
||||
assume(not (eq is False and order is True))
|
||||
|
||||
if cmp is not None:
|
||||
rv = cmp
|
||||
elif eq is True or eq is None:
|
||||
rv = order is None or order is True
|
||||
elif cmp is None and eq is None and order is None:
|
||||
rv = True
|
||||
elif cmp is None or eq is None:
|
||||
rv = False
|
||||
else:
|
||||
pytest.fail(
|
||||
"Unexpected state: cmp=%r eq=%r order=%r" % (cmp, eq, order)
|
||||
)
|
||||
|
||||
with pytest.deprecated_call() as dc:
|
||||
|
||||
@attr.s
|
||||
class C(object):
|
||||
x = attr.ib(cmp=cmp, eq=eq, order=order)
|
||||
|
||||
assert rv == attr.fields(C).x.cmp
|
||||
|
||||
if cmp is not None:
|
||||
# Remove warning from creating the attribute if cmp is not None.
|
||||
dc.pop()
|
||||
|
||||
w, = dc.list
|
||||
|
||||
assert (
|
||||
"The usage of `cmp` is deprecated and will be removed on or after "
|
||||
"2021-06-01. Please use `eq` and `order` instead."
|
||||
== w.message.args[0]
|
||||
)
|
||||
|
|
|
@ -29,8 +29,10 @@ from attr.validators import instance_of
|
|||
from .utils import simple_attr, simple_class
|
||||
|
||||
|
||||
CmpC = simple_class(cmp=True)
|
||||
CmpCSlots = simple_class(cmp=True, slots=True)
|
||||
EqC = simple_class(eq=True)
|
||||
EqCSlots = simple_class(eq=True, slots=True)
|
||||
OrderC = simple_class(order=True)
|
||||
OrderCSlots = simple_class(order=True, slots=True)
|
||||
ReprC = simple_class(repr=True)
|
||||
ReprCSlots = simple_class(repr=True, slots=True)
|
||||
|
||||
|
@ -38,10 +40,10 @@ ReprCSlots = simple_class(repr=True, slots=True)
|
|||
# implicitly. The "Cached" versions are the same, except with hash code
|
||||
# caching enabled
|
||||
HashC = simple_class(hash=True)
|
||||
HashCSlots = simple_class(hash=None, cmp=True, frozen=True, slots=True)
|
||||
HashCSlots = simple_class(hash=None, eq=True, frozen=True, slots=True)
|
||||
HashCCached = simple_class(hash=True, cache_hash=True)
|
||||
HashCSlotsCached = simple_class(
|
||||
hash=None, cmp=True, frozen=True, slots=True, cache_hash=True
|
||||
hash=None, eq=True, frozen=True, slots=True, cache_hash=True
|
||||
)
|
||||
# the cached hash code is stored slightly differently in this case
|
||||
# so it needs to be tested separately
|
||||
|
@ -77,23 +79,23 @@ class InitC(object):
|
|||
InitC = _add_init(InitC, False)
|
||||
|
||||
|
||||
class TestAddCmp(object):
|
||||
class TestEqOrder(object):
|
||||
"""
|
||||
Tests for `_add_cmp`.
|
||||
Tests for eq and order related methods.
|
||||
"""
|
||||
|
||||
@given(booleans())
|
||||
def test_cmp(self, slots):
|
||||
def test_eq_ignore_attrib(self, slots):
|
||||
"""
|
||||
If `cmp` is False, ignore that attribute.
|
||||
If `eq` is False for an attribute, ignore that attribute.
|
||||
"""
|
||||
C = make_class(
|
||||
"C", {"a": attr.ib(cmp=False), "b": attr.ib()}, slots=slots
|
||||
"C", {"a": attr.ib(eq=False), "b": attr.ib()}, slots=slots
|
||||
)
|
||||
|
||||
assert C(1, 2) == C(2, 2)
|
||||
|
||||
@pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
|
||||
@pytest.mark.parametrize("cls", [EqC, EqCSlots])
|
||||
def test_equal(self, cls):
|
||||
"""
|
||||
Equal objects are detected as equal.
|
||||
|
@ -101,7 +103,7 @@ class TestAddCmp(object):
|
|||
assert cls(1, 2) == cls(1, 2)
|
||||
assert not (cls(1, 2) != cls(1, 2))
|
||||
|
||||
@pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
|
||||
@pytest.mark.parametrize("cls", [EqC, EqCSlots])
|
||||
def test_unequal_same_class(self, cls):
|
||||
"""
|
||||
Unequal objects of correct type are detected as unequal.
|
||||
|
@ -109,21 +111,21 @@ class TestAddCmp(object):
|
|||
assert cls(1, 2) != cls(2, 1)
|
||||
assert not (cls(1, 2) == cls(2, 1))
|
||||
|
||||
@pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
|
||||
@pytest.mark.parametrize("cls", [EqC, EqCSlots])
|
||||
def test_unequal_different_class(self, cls):
|
||||
"""
|
||||
Unequal objects of different type are detected even if their attributes
|
||||
match.
|
||||
"""
|
||||
|
||||
class NotCmpC(object):
|
||||
class NotEqC(object):
|
||||
a = 1
|
||||
b = 2
|
||||
|
||||
assert cls(1, 2) != NotCmpC()
|
||||
assert not (cls(1, 2) == NotCmpC())
|
||||
assert cls(1, 2) != NotEqC()
|
||||
assert not (cls(1, 2) == NotEqC())
|
||||
|
||||
@pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
|
||||
@pytest.mark.parametrize("cls", [OrderC, OrderCSlots])
|
||||
def test_lt(self, cls):
|
||||
"""
|
||||
__lt__ compares objects as tuples of attribute values.
|
||||
|
@ -135,14 +137,14 @@ class TestAddCmp(object):
|
|||
]:
|
||||
assert cls(*a) < cls(*b)
|
||||
|
||||
@pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
|
||||
@pytest.mark.parametrize("cls", [OrderC, OrderCSlots])
|
||||
def test_lt_unordable(self, cls):
|
||||
"""
|
||||
__lt__ returns NotImplemented if classes differ.
|
||||
"""
|
||||
assert NotImplemented == (cls(1, 2).__lt__(42))
|
||||
|
||||
@pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
|
||||
@pytest.mark.parametrize("cls", [OrderC, OrderCSlots])
|
||||
def test_le(self, cls):
|
||||
"""
|
||||
__le__ compares objects as tuples of attribute values.
|
||||
|
@ -156,14 +158,14 @@ class TestAddCmp(object):
|
|||
]:
|
||||
assert cls(*a) <= cls(*b)
|
||||
|
||||
@pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
|
||||
@pytest.mark.parametrize("cls", [OrderC, OrderCSlots])
|
||||
def test_le_unordable(self, cls):
|
||||
"""
|
||||
__le__ returns NotImplemented if classes differ.
|
||||
"""
|
||||
assert NotImplemented == (cls(1, 2).__le__(42))
|
||||
|
||||
@pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
|
||||
@pytest.mark.parametrize("cls", [OrderC, OrderCSlots])
|
||||
def test_gt(self, cls):
|
||||
"""
|
||||
__gt__ compares objects as tuples of attribute values.
|
||||
|
@ -175,14 +177,14 @@ class TestAddCmp(object):
|
|||
]:
|
||||
assert cls(*a) > cls(*b)
|
||||
|
||||
@pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
|
||||
@pytest.mark.parametrize("cls", [OrderC, OrderCSlots])
|
||||
def test_gt_unordable(self, cls):
|
||||
"""
|
||||
__gt__ returns NotImplemented if classes differ.
|
||||
"""
|
||||
assert NotImplemented == (cls(1, 2).__gt__(42))
|
||||
|
||||
@pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
|
||||
@pytest.mark.parametrize("cls", [OrderC, OrderCSlots])
|
||||
def test_ge(self, cls):
|
||||
"""
|
||||
__ge__ compares objects as tuples of attribute values.
|
||||
|
@ -196,7 +198,7 @@ class TestAddCmp(object):
|
|||
]:
|
||||
assert cls(*a) >= cls(*b)
|
||||
|
||||
@pytest.mark.parametrize("cls", [CmpC, CmpCSlots])
|
||||
@pytest.mark.parametrize("cls", [OrderC, OrderCSlots])
|
||||
def test_ge_unordable(self, cls):
|
||||
"""
|
||||
__ge__ returns NotImplemented if classes differ.
|
||||
|
@ -347,7 +349,7 @@ class TestAddHash(object):
|
|||
# unhashable case
|
||||
with pytest.raises(TypeError) as e:
|
||||
make_class(
|
||||
"C", {}, hash=None, cmp=True, frozen=False, cache_hash=True
|
||||
"C", {}, hash=None, eq=True, frozen=False, cache_hash=True
|
||||
)
|
||||
assert exc_args == e.value.args
|
||||
|
||||
|
@ -380,13 +382,13 @@ class TestAddHash(object):
|
|||
assert hash(C(1, 2)) == hash(C(2, 2))
|
||||
|
||||
@given(booleans())
|
||||
def test_hash_attribute_mirrors_cmp(self, cmp):
|
||||
def test_hash_attribute_mirrors_eq(self, eq):
|
||||
"""
|
||||
If `hash` is None, the hash generation mirrors `cmp`.
|
||||
If `hash` is None, the hash generation mirrors `eq`.
|
||||
"""
|
||||
C = make_class("C", {"a": attr.ib(cmp=cmp)}, cmp=True, frozen=True)
|
||||
C = make_class("C", {"a": attr.ib(eq=eq)}, eq=True, frozen=True)
|
||||
|
||||
if cmp:
|
||||
if eq:
|
||||
assert C(1) != C(2)
|
||||
assert hash(C(1)) != hash(C(2))
|
||||
assert hash(C(1)) == hash(C(1))
|
||||
|
@ -395,18 +397,18 @@ class TestAddHash(object):
|
|||
assert hash(C(1)) == hash(C(2))
|
||||
|
||||
@given(booleans())
|
||||
def test_hash_mirrors_cmp(self, cmp):
|
||||
def test_hash_mirrors_eq(self, eq):
|
||||
"""
|
||||
If `hash` is None, the hash generation mirrors `cmp`.
|
||||
If `hash` is None, the hash generation mirrors `eq`.
|
||||
"""
|
||||
C = make_class("C", {"a": attr.ib()}, cmp=cmp, frozen=True)
|
||||
C = make_class("C", {"a": attr.ib()}, eq=eq, frozen=True)
|
||||
|
||||
i = C(1)
|
||||
|
||||
assert i == i
|
||||
assert hash(i) == hash(i)
|
||||
|
||||
if cmp:
|
||||
if eq:
|
||||
assert C(1) == C(1)
|
||||
assert hash(C(1)) == hash(C(1))
|
||||
else:
|
||||
|
@ -777,7 +779,7 @@ class TestNothing(object):
|
|||
assert 1 != _Nothing()
|
||||
|
||||
|
||||
@attr.s(hash=True, cmp=True)
|
||||
@attr.s(hash=True, order=True)
|
||||
class C(object):
|
||||
pass
|
||||
|
||||
|
@ -786,7 +788,7 @@ class C(object):
|
|||
OriginalC = C
|
||||
|
||||
|
||||
@attr.s(hash=True, cmp=True)
|
||||
@attr.s(hash=True, order=True)
|
||||
class C(object):
|
||||
pass
|
||||
|
||||
|
|
|
@ -25,6 +25,21 @@ MAPPING_TYPES = (dict, OrderedDict)
|
|||
SEQUENCE_TYPES = (list, tuple)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", name="C")
|
||||
def fixture_C():
|
||||
"""
|
||||
Return a simple but fully featured attrs class with an x and a y attribute.
|
||||
"""
|
||||
import attr
|
||||
|
||||
@attr.s
|
||||
class C(object):
|
||||
x = attr.ib()
|
||||
y = attr.ib()
|
||||
|
||||
return C
|
||||
|
||||
|
||||
class TestAsDict(object):
|
||||
"""
|
||||
Tests for `asdict`.
|
||||
|
|
|
@ -14,7 +14,7 @@ from operator import attrgetter
|
|||
|
||||
import pytest
|
||||
|
||||
from hypothesis import given
|
||||
from hypothesis import assume, given
|
||||
from hypothesis.strategies import booleans, integers, lists, sampled_from, text
|
||||
|
||||
import attr
|
||||
|
@ -28,6 +28,7 @@ from attr._make import (
|
|||
_Attributes,
|
||||
_ClassBuilder,
|
||||
_CountingAttr,
|
||||
_determine_eq_order,
|
||||
_transform_attrs,
|
||||
and_,
|
||||
fields,
|
||||
|
@ -44,6 +45,7 @@ from attr.exceptions import (
|
|||
from .strategies import (
|
||||
gen_attr_names,
|
||||
list_of_attrs,
|
||||
optional_bool,
|
||||
simple_attrs,
|
||||
simple_attrs_with_metadata,
|
||||
simple_attrs_without_metadata,
|
||||
|
@ -216,8 +218,9 @@ class TestTransformAttrs(object):
|
|||
"No mandatory attributes allowed after an attribute with a "
|
||||
"default value or factory. Attribute in question: Attribute"
|
||||
"(name='y', default=NOTHING, validator=None, repr=True, "
|
||||
"cmp=True, hash=None, init=True, metadata=mappingproxy({}), "
|
||||
"type=None, converter=None, kw_only=False)",
|
||||
"eq=True, order=True, hash=None, init=True, "
|
||||
"metadata=mappingproxy({}), type=None, converter=None, "
|
||||
"kw_only=False)",
|
||||
) == e.value.args
|
||||
|
||||
def test_kw_only(self):
|
||||
|
@ -411,21 +414,30 @@ class TestAttributes(object):
|
|||
"arg_name, method_name",
|
||||
[
|
||||
("repr", "__repr__"),
|
||||
("cmp", "__eq__"),
|
||||
("eq", "__eq__"),
|
||||
("order", "__le__"),
|
||||
("hash", "__hash__"),
|
||||
("init", "__init__"),
|
||||
],
|
||||
)
|
||||
def test_respects_add_arguments(self, arg_name, method_name):
|
||||
"""
|
||||
If a certain `add_XXX` is `False`, `__XXX__` is not added to the class.
|
||||
If a certain `XXX` is `False`, `__XXX__` is not added to the class.
|
||||
"""
|
||||
# Set the method name to a sentinel and check whether it has been
|
||||
# overwritten afterwards.
|
||||
sentinel = object()
|
||||
|
||||
am_args = {"repr": True, "cmp": True, "hash": True, "init": True}
|
||||
am_args = {
|
||||
"repr": True,
|
||||
"eq": True,
|
||||
"order": True,
|
||||
"hash": True,
|
||||
"init": True,
|
||||
}
|
||||
am_args[arg_name] = False
|
||||
if arg_name == "eq":
|
||||
am_args["order"] = False
|
||||
|
||||
class C(object):
|
||||
x = attr.ib()
|
||||
|
@ -918,16 +930,17 @@ class TestFields(object):
|
|||
Tests for `fields`.
|
||||
"""
|
||||
|
||||
@given(simple_classes())
|
||||
def test_instance(self, C):
|
||||
"""
|
||||
Raises `TypeError` on non-classes.
|
||||
"""
|
||||
with pytest.raises(TypeError) as e:
|
||||
fields(C(1, 2))
|
||||
fields(C())
|
||||
|
||||
assert "Passed object must be a class." == e.value.args[0]
|
||||
|
||||
def test_handler_non_attrs_class(self, C):
|
||||
def test_handler_non_attrs_class(self):
|
||||
"""
|
||||
Raises `ValueError` if passed a non-``attrs`` instance.
|
||||
"""
|
||||
|
@ -959,16 +972,17 @@ class TestFieldsDict(object):
|
|||
Tests for `fields_dict`.
|
||||
"""
|
||||
|
||||
@given(simple_classes())
|
||||
def test_instance(self, C):
|
||||
"""
|
||||
Raises `TypeError` on non-classes.
|
||||
"""
|
||||
with pytest.raises(TypeError) as e:
|
||||
fields_dict(C(1, 2))
|
||||
fields_dict(C())
|
||||
|
||||
assert "Passed object must be a class." == e.value.args[0]
|
||||
|
||||
def test_handler_non_attrs_class(self, C):
|
||||
def test_handler_non_attrs_class(self):
|
||||
"""
|
||||
Raises `ValueError` if passed a non-``attrs`` instance.
|
||||
"""
|
||||
|
@ -1341,7 +1355,8 @@ class TestClassBuilder(object):
|
|||
)
|
||||
|
||||
cls = (
|
||||
b.add_cmp()
|
||||
b.add_eq()
|
||||
.add_order()
|
||||
.add_hash()
|
||||
.add_init()
|
||||
.add_repr("ns")
|
||||
|
@ -1427,7 +1442,7 @@ class TestClassBuilder(object):
|
|||
@attr.s(slots=True)
|
||||
class C(object):
|
||||
__weakref__ = attr.ib(
|
||||
init=False, hash=False, repr=False, cmp=False
|
||||
init=False, hash=False, repr=False, eq=False, order=False
|
||||
)
|
||||
|
||||
assert C() == copy.deepcopy(C())
|
||||
|
@ -1452,9 +1467,9 @@ class TestClassBuilder(object):
|
|||
assert [C2] == C.__subclasses__()
|
||||
|
||||
|
||||
class TestMakeCmp:
|
||||
class TestMakeOrder:
|
||||
"""
|
||||
Tests for _make_cmp().
|
||||
Tests for _make_order().
|
||||
"""
|
||||
|
||||
def test_subclasses_cannot_be_compared(self):
|
||||
|
@ -1500,3 +1515,57 @@ class TestMakeCmp:
|
|||
|
||||
with pytest.raises(TypeError):
|
||||
a > b
|
||||
|
||||
|
||||
class TestDetermineEqOrder(object):
|
||||
def test_default(self):
|
||||
"""
|
||||
If all are set to None, do the default: True, True
|
||||
"""
|
||||
assert (True, True) == _determine_eq_order(None, None, None)
|
||||
|
||||
@pytest.mark.parametrize("eq", [True, False])
|
||||
def test_order_mirrors_eq_by_default(self, eq):
|
||||
"""
|
||||
If order is None, it mirrors eq.
|
||||
"""
|
||||
assert (eq, eq) == _determine_eq_order(None, eq, None)
|
||||
|
||||
def test_order_without_eq(self):
|
||||
"""
|
||||
eq=False, order=True raises a meaningful ValueError.
|
||||
"""
|
||||
with pytest.raises(
|
||||
ValueError, match="`order` can only be True if `eq` is True too."
|
||||
):
|
||||
_determine_eq_order(None, False, True)
|
||||
|
||||
@given(cmp=booleans(), eq=optional_bool, order=optional_bool)
|
||||
def test_mix(self, cmp, eq, order):
|
||||
"""
|
||||
If cmp is not None, eq and order must be None and vice versa.
|
||||
"""
|
||||
assume(eq is not None or order is not None)
|
||||
|
||||
with pytest.raises(
|
||||
ValueError, match="Don't mix `cmp` with `eq' and `order`."
|
||||
):
|
||||
_determine_eq_order(cmp, eq, order)
|
||||
|
||||
def test_cmp_deprecated(self):
|
||||
"""
|
||||
Passing a cmp that is not None raises a DeprecationWarning.
|
||||
"""
|
||||
with pytest.deprecated_call() as dc:
|
||||
|
||||
@attr.s(cmp=True)
|
||||
class C(object):
|
||||
pass
|
||||
|
||||
w, = dc.list
|
||||
|
||||
assert (
|
||||
"The usage of `cmp` is deprecated and will be removed on or after "
|
||||
"2021-06-01. Please use `eq` and `order` instead."
|
||||
== w.message.args[0]
|
||||
)
|
||||
|
|
|
@ -272,7 +272,9 @@ def test_bare_inheritance_from_slots():
|
|||
Inheriting from a bare attrs slotted class works.
|
||||
"""
|
||||
|
||||
@attr.s(init=False, cmp=False, hash=False, repr=False, slots=True)
|
||||
@attr.s(
|
||||
init=False, eq=False, order=False, hash=False, repr=False, slots=True
|
||||
)
|
||||
class C1BareSlots(object):
|
||||
x = attr.ib(validator=attr.validators.instance_of(int))
|
||||
y = attr.ib()
|
||||
|
@ -288,7 +290,7 @@ def test_bare_inheritance_from_slots():
|
|||
def staticmethod():
|
||||
return "staticmethod"
|
||||
|
||||
@attr.s(init=False, cmp=False, hash=False, repr=False)
|
||||
@attr.s(init=False, eq=False, order=False, hash=False, repr=False)
|
||||
class C1Bare(object):
|
||||
x = attr.ib(validator=attr.validators.instance_of(int))
|
||||
y = attr.ib()
|
||||
|
@ -533,7 +535,9 @@ def tests_weakref_does_not_add_with_weakref_attribute():
|
|||
|
||||
@attr.s(slots=True, weakref_slot=True)
|
||||
class C(object):
|
||||
__weakref__ = attr.ib(init=False, hash=False, repr=False, cmp=False)
|
||||
__weakref__ = attr.ib(
|
||||
init=False, hash=False, repr=False, eq=False, order=False
|
||||
)
|
||||
|
||||
c = C()
|
||||
w = weakref.ref(c)
|
||||
|
|
|
@ -166,3 +166,10 @@ class WithCustomRepr:
|
|||
b = attr.ib(repr=False)
|
||||
c = attr.ib(repr=lambda value: "c is for cookie")
|
||||
d = attr.ib(repr=str)
|
||||
|
||||
|
||||
# Check some of our own types
|
||||
@attr.s(eq=True, order=False)
|
||||
class OrderFlags:
|
||||
a = attr.ib(eq=False, order=False)
|
||||
b = attr.ib(eq=True, order=True)
|
||||
|
|
|
@ -9,7 +9,8 @@ from attr._make import NOTHING, make_class
|
|||
|
||||
|
||||
def simple_class(
|
||||
cmp=False,
|
||||
eq=False,
|
||||
order=False,
|
||||
repr=False,
|
||||
hash=False,
|
||||
str=False,
|
||||
|
@ -23,7 +24,8 @@ def simple_class(
|
|||
return make_class(
|
||||
"C",
|
||||
["a", "b"],
|
||||
cmp=cmp,
|
||||
eq=eq or order,
|
||||
order=order,
|
||||
repr=repr,
|
||||
hash=hash,
|
||||
init=True,
|
||||
|
@ -39,7 +41,7 @@ def simple_attr(
|
|||
default=NOTHING,
|
||||
validator=None,
|
||||
repr=True,
|
||||
cmp=True,
|
||||
eq=True,
|
||||
hash=None,
|
||||
init=True,
|
||||
converter=None,
|
||||
|
@ -53,7 +55,8 @@ def simple_attr(
|
|||
default=default,
|
||||
validator=validator,
|
||||
repr=repr,
|
||||
cmp=cmp,
|
||||
cmp=None,
|
||||
eq=eq,
|
||||
hash=hash,
|
||||
init=init,
|
||||
converter=converter,
|
||||
|
|
Loading…
Reference in New Issue