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):