From 947bfb542104209a587280701d8cb389c813459d Mon Sep 17 00:00:00 2001 From: Ray Zhang Date: Sun, 30 Jan 2022 23:57:33 -0500 Subject: [PATCH] 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 0bd3f5cba641046e6b3342093301c755835674d8. * Address comments Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Hynek Schlawack --- changelog.d/909.change.rst | 1 + src/attr/_make.py | 17 ++++++++++++----- tests/test_make.py | 21 +++++++++++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 changelog.d/909.change.rst diff --git a/changelog.d/909.change.rst b/changelog.d/909.change.rst new file mode 100644 index 000000000..359d1207b --- /dev/null +++ b/changelog.d/909.change.rst @@ -0,0 +1 @@ +If an eq key is defined, it is also used before hashing the attribute. diff --git a/src/attr/_make.py b/src/attr/_make.py index 915c5e663..19a354204 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -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. @@ -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((" @@ -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) @@ -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): diff --git a/tests/test_make.py b/tests/test_make.py index 6cc4f059f..cdca30d08 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -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):