Skip to content

Commit

Permalink
Fix hashing for custom eq objects (#909)
Browse files Browse the repository at this point in the history
* Add hashing for eq'd objects

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add coverage to test

* oops

* Add changelog message

* Revert "Add changelog message"

This reverts commit 0bd3f5c.

* Address comments

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Hynek Schlawack <hs@ox.cx>
  • Loading branch information
3 people committed Jan 31, 2022
1 parent 976b828 commit 947bfb5
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 5 deletions.
1 change: 1 addition & 0 deletions changelog.d/909.change.rst
@@ -0,0 +1 @@
If an eq key is defined, it is also used before hashing the attribute.
17 changes: 12 additions & 5 deletions src/attr/_make.py
Expand Up @@ -325,13 +325,11 @@ def _compile_and_eval(script, globs, locs=None, filename=""):
eval(bytecode, globs, locs)


def _make_method(name, script, filename, globs=None):
def _make_method(name, script, filename, globs):
"""
Create the method with the script given and return the method object.
"""
locs = {}
if globs is None:
globs = {}

# In order of debuggers like PDB being able to step through the code,
# we add a fake linecache entry.
Expand Down Expand Up @@ -1680,6 +1678,8 @@ def _make_hash(cls, attrs, frozen, cache_hash):

unique_filename = _generate_unique_filename(cls, "hash")
type_hash = hash(unique_filename)
# If eq is custom generated, we need to include the functions in globs
globs = {}

hash_def = "def __hash__(self"
hash_func = "hash(("
Expand Down Expand Up @@ -1714,7 +1714,14 @@ def append_hash_computation_lines(prefix, indent):
)

for a in attrs:
method_lines.append(indent + " self.%s," % a.name)
if a.eq_key:
cmp_name = "_%s_key" % (a.name,)
globs[cmp_name] = a.eq_key
method_lines.append(
indent + " %s(self.%s)," % (cmp_name, a.name)
)
else:
method_lines.append(indent + " self.%s," % a.name)

method_lines.append(indent + " " + closing_braces)

Expand All @@ -1734,7 +1741,7 @@ def append_hash_computation_lines(prefix, indent):
append_hash_computation_lines("return ", tab)

script = "\n".join(method_lines)
return _make_method("__hash__", script, unique_filename)
return _make_method("__hash__", script, unique_filename, globs)


def _add_hash(cls, attrs):
Expand Down
21 changes: 21 additions & 0 deletions tests/test_make.py
Expand Up @@ -2057,6 +2057,27 @@ def __repr__(self):

assert "hi" == repr(C(42))

@pytest.mark.parametrize("slots", [True, False])
@pytest.mark.parametrize("frozen", [True, False])
def test_hash_uses_eq(self, slots, frozen):
"""
If eq is passed in, then __hash__ should use the eq callable
to generate the hash code.
"""

@attr.s(slots=slots, frozen=frozen, hash=True)
class C(object):
x = attr.ib(eq=str)

@attr.s(slots=slots, frozen=frozen, hash=True)
class D(object):
x = attr.ib()

# These hashes should be the same because 1 is turned into
# string before hashing.
assert hash(C("1")) == hash(C(1))
assert hash(D("1")) != hash(D(1))

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

0 comments on commit 947bfb5

Please sign in to comment.