Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make auto_exc=True classes hashable by ID #563

Merged
merged 3 commits into from
Aug 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/543.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``@attr.s(auto_exc=True)`` now generates classes that are hashable by ID, as the documentation always claimed it would.
1 change: 1 addition & 0 deletions changelog.d/563.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``@attr.s(auto_exc=True)`` now generates classes that are hashable by ID, as the documentation always claimed it would.
12 changes: 6 additions & 6 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -912,20 +912,20 @@ def wrap(cls):
raise TypeError(
"Invalid value for hash. Must be True, False, or None."
)
elif hash is False or (hash is None and cmp is False):
elif hash is False or (hash is None and cmp is False) or is_exc:
# Don't do anything. Should fall back to __object__'s __hash__
# which is by id.
if cache_hash:
raise TypeError(
"Invalid value for cache_hash. To use hash caching,"
" hashing must be either explicitly or implicitly "
"enabled."
)
elif (
hash is True
or (hash is None and cmp is True and frozen is True)
and is_exc is False
):
elif hash is True or (hash is None and cmp is True and frozen is True):
# Build a __hash__ if told so, or if it's safe.
builder.add_hash()
else:
# Raise TypeError on attempts to hash.
if cache_hash:
raise TypeError(
"Invalid value for cache_hash. To use hash caching,"
Expand Down
30 changes: 18 additions & 12 deletions tests/test_dark_magic.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,9 +515,8 @@ class C(object):
@pytest.mark.parametrize("frozen", [True, False])
def test_auto_exc(self, slots, frozen):
"""
Classes with auto_exc=True have a Exception-style __str__, are neither
comparable nor hashable, and store the fields additionally in
self.args.
Classes with auto_exc=True have a Exception-style __str__, compare and
hash by id, and store the fields additionally in self.args.
"""

@attr.s(auto_exc=True, slots=slots, frozen=frozen)
Expand Down Expand Up @@ -545,21 +544,28 @@ class FooError(Exception):
assert FooErrorMade(1, "foo") != FooErrorMade(1, "foo")

for cls in (FooError, FooErrorMade):
with pytest.raises(cls) as ei:
with pytest.raises(cls) as ei1:
raise cls(1, "foo")

e = ei.value
with pytest.raises(cls) as ei2:
raise cls(1, "foo")

e1 = ei1.value
e2 = ei2.value

assert e is e
assert e == e
assert "(1, 'foo')" == str(e)
assert (1, "foo") == e.args
assert e1 is e1
assert e1 == e1
assert e2 == e2
assert e1 != e2
assert "(1, 'foo')" == str(e1) == str(e2)
assert (1, "foo") == e1.args == e2.args

with pytest.raises(TypeError):
hash(e)
hash(e1) == hash(e1)
hash(e2) == hash(e2)

if not frozen:
deepcopy(e)
deepcopy(e1)
deepcopy(e2)

@pytest.mark.parametrize("slots", [True, False])
@pytest.mark.parametrize("frozen", [True, False])
Expand Down