Skip to content

Commit 947bfb5

Browse files
OneRaynyDaypre-commit-ci[bot]hynek
authored
Fix hashing for custom eq objects (#909)
* 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>
1 parent 976b828 commit 947bfb5

3 files changed

Lines changed: 34 additions & 5 deletions

File tree

changelog.d/909.change.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
If an eq key is defined, it is also used before hashing the attribute.

src/attr/_make.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -325,13 +325,11 @@ def _compile_and_eval(script, globs, locs=None, filename=""):
325325
eval(bytecode, globs, locs)
326326

327327

328-
def _make_method(name, script, filename, globs=None):
328+
def _make_method(name, script, filename, globs):
329329
"""
330330
Create the method with the script given and return the method object.
331331
"""
332332
locs = {}
333-
if globs is None:
334-
globs = {}
335333

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

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

16841684
hash_def = "def __hash__(self"
16851685
hash_func = "hash(("
@@ -1714,7 +1714,14 @@ def append_hash_computation_lines(prefix, indent):
17141714
)
17151715

17161716
for a in attrs:
1717-
method_lines.append(indent + " self.%s," % a.name)
1717+
if a.eq_key:
1718+
cmp_name = "_%s_key" % (a.name,)
1719+
globs[cmp_name] = a.eq_key
1720+
method_lines.append(
1721+
indent + " %s(self.%s)," % (cmp_name, a.name)
1722+
)
1723+
else:
1724+
method_lines.append(indent + " self.%s," % a.name)
17181725

17191726
method_lines.append(indent + " " + closing_braces)
17201727

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

17361743
script = "\n".join(method_lines)
1737-
return _make_method("__hash__", script, unique_filename)
1744+
return _make_method("__hash__", script, unique_filename, globs)
17381745

17391746

17401747
def _add_hash(cls, attrs):

tests/test_make.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2057,6 +2057,27 @@ def __repr__(self):
20572057

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

2060+
@pytest.mark.parametrize("slots", [True, False])
2061+
@pytest.mark.parametrize("frozen", [True, False])
2062+
def test_hash_uses_eq(self, slots, frozen):
2063+
"""
2064+
If eq is passed in, then __hash__ should use the eq callable
2065+
to generate the hash code.
2066+
"""
2067+
2068+
@attr.s(slots=slots, frozen=frozen, hash=True)
2069+
class C(object):
2070+
x = attr.ib(eq=str)
2071+
2072+
@attr.s(slots=slots, frozen=frozen, hash=True)
2073+
class D(object):
2074+
x = attr.ib()
2075+
2076+
# These hashes should be the same because 1 is turned into
2077+
# string before hashing.
2078+
assert hash(C("1")) == hash(C(1))
2079+
assert hash(D("1")) != hash(D(1))
2080+
20602081
@pytest.mark.parametrize("slots", [True, False])
20612082
@pytest.mark.parametrize("frozen", [True, False])
20622083
def test_detect_auto_hash(self, slots, frozen):

0 commit comments

Comments
 (0)