Skip to content

Commit

Permalink
Mirror cmp behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
hynek committed Feb 18, 2017
1 parent 98afb0f commit 095fbe9
Show file tree
Hide file tree
Showing 3 changed files with 30 additions and 14 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ Deprecations:
Changes:
^^^^^^^^

- Fix hashing behavior by setting ``__hash__`` to ``None`` by default and mirror ``cmp`` and ``hash`` in attributes.
- Fix default hashing behavior.
Now *hash* mirrors the value of *cmp* and classes are unhashable by default.
`#136 <https://github.com/hynek/attrs/issues/136>`_
`#142 <https://github.com/hynek/attrs/issues/142>`_
- Add ``attr.evolve`` that, given an instance of an ``attrs`` class and field changes as keyword arguments, will instantiate a copy of the given instance with the changes applied.
Expand Down
24 changes: 11 additions & 13 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,21 +255,19 @@ def attributes(maybe_cls=None, these=None, repr_ns=None,
a tuple of its ``attrs`` attributes. But the attributes are *only*
compared, if the type of both classes is *identical*!
:param hash: If ``None`` (default), the ``__hash__`` method is generated
according how *cmp* and *frozen* are set. You will receive one if
*both* are ``True``.
according how *cmp* 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
None, marking it unhashable (which it is).
3. If *cmp* is False, ``__hash__`` will be left untouched meaning the
``__hash__`` method of the superclass will be used (if superclass is
``object``, this means it will fall back to id-based hashing.).
Although not recommended, you can decide for yourself and force
``attrs`` to create one (e.g. if the class is immutable even though you
didn't freeze it programmatically) by passing ``True`` or not (e.g. if
you want to use the superclass's ``__hash__`` method be it Python's
build-in id-based hashing or your own). Both of these cases are rather
special and should be used carefully.
Please note that setting *hash* to ``False`` means that the
superclass's ``__hash__`` function is used. If you set it to ``None``,
and your class is not *both* ``cmp=True`` and ``frozen=True``, the
``__hash__`` method is set to ``None``, making it not hashable (which
it is).
didn't freeze it programmatically) by passing ``True`` or not. Both of
these cases are rather special and should be used carefully.
See the `Python documentation \
<https://docs.python.org/3/reference/datamodel.html#object.__hash__>`_
Expand Down Expand Up @@ -340,7 +338,7 @@ def wrap(cls):
raise TypeError(
"Invalid value for hash. Must be True, False, or None."
)
elif hash is False:
elif hash is False or (hash is None and cmp is False):
pass
elif hash is True or (hash is None and cmp is True and frozen is True):
cls = _add_hash(cls)
Expand Down
17 changes: 17 additions & 0 deletions tests/test_dunders.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,23 @@ def test_hash_attribute_mirrors_cmp(self, cmp):
assert C(1) == C(2)
assert hash(C(1)) == hash(C(2))

@given(booleans())
def test_hash_mirrors_cmp(self, cmp):
"""
If `hash` is None, the hash generation mirrors `cmp`.
"""
C = make_class("C", {"a": attr()}, cmp=cmp, frozen=True)

i = C(1)
assert i == i
if cmp:
assert C(1) == C(1)
assert hash(C(1)) == hash(C(1))
else:
assert C(1) != C(1)
assert hash(C(1)) != hash(C(1))
assert hash(i) == hash(i)

@pytest.mark.parametrize("cls", [HashC, HashCSlots])
def test_hash_works(self, cls):
"""
Expand Down

0 comments on commit 095fbe9

Please sign in to comment.