From 4dc48da9dbdea09364a67c98f4f3d3b4499d4b7d Mon Sep 17 00:00:00 2001 From: Xavier Fernandez Date: Sat, 31 Oct 2020 15:54:54 +0100 Subject: [PATCH 1/2] utils: make Hashes object hashable --- src/pip/_internal/utils/hashes.py | 23 ++++++++++++++++++++++- tests/unit/test_utils.py | 10 ++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index b306dafe7d7..4d90f5bfda4 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -39,7 +39,12 @@ def __init__(self, hashes=None): :param hashes: A dict of algorithm names pointing to lists of allowed hex digests """ - self._allowed = {} if hashes is None else hashes + allowed = {} + if hashes is not None: + for alg, keys in hashes.items(): + # Make sure values are always sorted (to ease equality checks) + allowed[alg] = sorted(keys) + self._allowed = allowed def __and__(self, other): # type: (Hashes) -> Hashes @@ -128,6 +133,22 @@ def __bool__(self): # type: () -> bool return self.__nonzero__() + def __eq__(self, other): + # type: (object) -> bool + if not isinstance(other, Hashes): + return NotImplemented + return self._allowed == other._allowed + + def __hash__(self): + # type: () -> int + return hash( + ",".join(sorted( + ":".join((alg, digest)) + for alg, digest_list in self._allowed.items() + for digest in digest_list + )) + ) + class MissingHashes(Hashes): """A workalike for Hashes used when we're missing a hash for a requirement diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 14b4d74820f..1996b35cb37 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -541,6 +541,16 @@ def test_non_zero(self): assert not Hashes() assert not Hashes({}) + def test_equality(self): + assert Hashes() == Hashes() + assert Hashes({'sha256': ['abcd']}) == Hashes({'sha256': ['abcd']}) + assert Hashes({'sha256': ['ab', 'cd']}) == Hashes({'sha256': ['cd', 'ab']}) + + def test_hash(self): + cache = {} + cache[Hashes({'sha256': ['ab', 'cd']})] = 42 + assert cache[Hashes({'sha256': ['ab', 'cd']})] == 42 + class TestEncoding(object): """Tests for pip._internal.utils.encoding""" From 5ec275fca2b8a1944c2b0a19ae4c334febd4929f Mon Sep 17 00:00:00 2001 From: Xavier Fernandez Date: Sat, 31 Oct 2020 16:00:11 +0100 Subject: [PATCH 2/2] Cache find_best_candidate results This is possible because self.make_candidate_evaluator only depends on: - the function arguments which are keys to the cache - self._target_python which never changes during a pip resolution - self._candidate_prefs which never changes during a pip resolution On a fresh install, pip install runs on my machine in: master (a0e34e9cf707c5) ======================= 0m33.058s 0m34.105s 0m32.426s This commit =========== 0m15.860s 0m16.254s 0m15.910s pip 20.2.4 - legacy resolver ============================ 0m15.145s 0m15.040s 0m15.152s --- src/pip/_internal/index/package_finder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index b361e194d75..9f39631dde2 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -863,6 +863,7 @@ def make_candidate_evaluator( hashes=hashes, ) + @lru_cache(maxsize=None) def find_best_candidate( self, project_name, # type: str