From e8eda16ed84fa861dccb490167010db49a6828c8 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 19 Jul 2019 22:07:49 -0400 Subject: [PATCH 1/7] Rename some PackageFinder "best candidate" classes and methods: * Rename FoundCandidates to BestCandidateResult. * Rename CandidateEvaluator's make_found_candidates() to compute_best_candidate(). * Rename CandidateEvaluator's get_best_candidate() to sort_best_candidate(). * Rename PackageFinder's find_candidates() to find_best_candidate(). --- src/pip/_internal/commands/list.py | 2 +- src/pip/_internal/index.py | 45 ++++++++++++++--------------- src/pip/_internal/utils/outdated.py | 6 ++-- tests/unit/test_index.py | 28 +++++++++--------- tests/unit/test_unit_outdated.py | 6 ++-- 5 files changed, 44 insertions(+), 43 deletions(-) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index aacd5680ca1..b86e38d1c20 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -192,7 +192,7 @@ def iter_packages_latest_infos(self, packages, options): evaluator = finder.make_candidate_evaluator( project_name=dist.project_name, ) - best_candidate = evaluator.get_best_candidate(all_candidates) + best_candidate = evaluator.sort_best_candidate(all_candidates) if best_candidate is None: continue diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index c5bc3bc3428..12f1071d414 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -69,7 +69,7 @@ SecureOrigin = Tuple[str, str, Optional[str]] -__all__ = ['FormatControl', 'FoundCandidates', 'PackageFinder'] +__all__ = ['FormatControl', 'BestCandidateResult', 'PackageFinder'] SECURE_ORIGINS = [ @@ -568,6 +568,9 @@ def create( :param target_python: The target Python interpreter to use when checking compatibility. If None (the default), a TargetPython object will be constructed from the running Python. + :param specifier: An optional object implementing `filter` + (e.g. `packaging.specifiers.SpecifierSet`) to filter applicable + versions. :param hashes: An optional collection of allowed hashes. """ if target_python is None: @@ -643,21 +646,17 @@ def get_applicable_candidates( project_name=self._project_name, ) - def make_found_candidates( + def compute_best_candidate( self, candidates, # type: List[InstallationCandidate] ): - # type: (...) -> FoundCandidates + # type: (...) -> BestCandidateResult """ - Create and return a `FoundCandidates` instance. - - :param specifier: An optional object implementing `filter` - (e.g. `packaging.specifiers.SpecifierSet`) to filter applicable - versions. + Compute and return a `BestCandidateResult` instance. """ applicable_candidates = self.get_applicable_candidates(candidates) - return FoundCandidates( + return BestCandidateResult( candidates, applicable_candidates=applicable_candidates, evaluator=self, @@ -723,7 +722,7 @@ def _sort_key(self, candidate): build_tag, pri, ) - def get_best_candidate( + def sort_best_candidate( self, candidates, # type: List[InstallationCandidate] ): @@ -754,11 +753,11 @@ def get_best_candidate( return best_candidate -class FoundCandidates(object): - """A collection of candidates, returned by `PackageFinder.find_candidates`. +class BestCandidateResult(object): + """A collection of candidates, returned by `PackageFinder.find_best_candidate`. This class is only intended to be instantiated by CandidateEvaluator's - `make_found_candidates()` method. + `compute_best_candidate()` method. """ def __init__( @@ -796,7 +795,7 @@ def get_best(self): candidates are found. """ candidates = list(self.iter_applicable()) - return self._evaluator.get_best_candidate(candidates) + return self._evaluator.sort_best_candidate(candidates) class PackageFinder(object): @@ -1174,20 +1173,20 @@ def make_candidate_evaluator( hashes=hashes, ) - def find_candidates( + def find_best_candidate( self, project_name, # type: str specifier=None, # type: Optional[specifiers.BaseSpecifier] hashes=None, # type: Optional[Hashes] ): - # type: (...) -> FoundCandidates + # type: (...) -> BestCandidateResult """Find matches for the given project and specifier. :param specifier: An optional object implementing `filter` (e.g. `packaging.specifiers.SpecifierSet`) to filter applicable versions. - :return: A `FoundCandidates` instance. + :return: A `BestCandidateResult` instance. """ candidates = self.find_all_candidates(project_name) candidate_evaluator = self.make_candidate_evaluator( @@ -1195,7 +1194,7 @@ def find_candidates( specifier=specifier, hashes=hashes, ) - return candidate_evaluator.make_found_candidates(candidates) + return candidate_evaluator.compute_best_candidate(candidates) def find_requirement(self, req, upgrade): # type: (InstallRequirement, bool) -> Optional[Link] @@ -1206,10 +1205,10 @@ def find_requirement(self, req, upgrade): Raises DistributionNotFound or BestVersionAlreadyInstalled otherwise """ hashes = req.hashes(trust_internet=False) - candidates = self.find_candidates( + best_candidate_result = self.find_best_candidate( req.name, specifier=req.specifier, hashes=hashes, ) - best_candidate = candidates.get_best() + best_candidate = best_candidate_result.get_best() installed_version = None # type: Optional[_BaseVersion] if req.satisfied_by is not None: @@ -1230,7 +1229,7 @@ def _format_versions(cand_iter): 'Could not find a version that satisfies the requirement %s ' '(from versions: %s)', req, - _format_versions(candidates.iter_all()), + _format_versions(best_candidate_result.iter_all()), ) raise DistributionNotFound( @@ -1265,14 +1264,14 @@ def _format_versions(cand_iter): 'Installed version (%s) is most up-to-date (past versions: ' '%s)', installed_version, - _format_versions(candidates.iter_applicable()), + _format_versions(best_candidate_result.iter_applicable()), ) raise BestVersionAlreadyInstalled logger.debug( 'Using version %s (newest of versions: %s)', best_candidate.version, - _format_versions(candidates.iter_applicable()), + _format_versions(best_candidate_result.iter_applicable()), ) return best_candidate.link diff --git a/src/pip/_internal/utils/outdated.py b/src/pip/_internal/utils/outdated.py index 2b10aeff6bb..67971ad69c3 100644 --- a/src/pip/_internal/utils/outdated.py +++ b/src/pip/_internal/utils/outdated.py @@ -139,10 +139,10 @@ def pip_version_check(session, options): trusted_hosts=options.trusted_hosts, session=session, ) - candidate = finder.find_candidates("pip").get_best() - if candidate is None: + best_candidate = finder.find_best_candidate("pip").get_best() + if best_candidate is None: return - pypi_version = str(candidate.version) + pypi_version = str(best_candidate.version) # save that we've performed a check state.save(pypi_version, current_time) diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index 4d7b5933117..79ec3f7b522 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -387,7 +387,7 @@ def test_get_applicable_candidates__hashes( actual_versions = [str(c.version) for c in actual] assert actual_versions == expected_versions - def test_make_found_candidates(self): + def test_compute_best_candidate(self): specifier = SpecifierSet('<= 1.11') versions = ['1.10', '1.11', '1.12'] candidates = [ @@ -397,16 +397,16 @@ def test_make_found_candidates(self): 'my-project', specifier=specifier, ) - found_candidates = evaluator.make_found_candidates(candidates) + result = evaluator.compute_best_candidate(candidates) - assert found_candidates._candidates == candidates - assert found_candidates._evaluator is evaluator + assert result._candidates == candidates + assert result._evaluator is evaluator expected_applicable = candidates[:2] assert [str(c.version) for c in expected_applicable] == [ '1.10', '1.11', ] - assert found_candidates._applicable_candidates == expected_applicable + assert result._applicable_candidates == expected_applicable @pytest.mark.parametrize('hex_digest, expected', [ # Test a link with no hash. @@ -448,15 +448,15 @@ def test_sort_key__is_yanked(self, yanked_reason, expected): actual = sort_value[1] assert actual == expected - def test_get_best_candidate__no_candidates(self): + def test_sort_best_candidate__no_candidates(self): """ Test passing an empty list. """ evaluator = CandidateEvaluator.create('my-project') - actual = evaluator.get_best_candidate([]) + actual = evaluator.sort_best_candidate([]) assert actual is None - def test_get_best_candidate__all_yanked(self, caplog): + def test_sort_best_candidate__all_yanked(self, caplog): """ Test all candidates yanked. """ @@ -468,7 +468,7 @@ def test_get_best_candidate__all_yanked(self, caplog): ] expected_best = candidates[1] evaluator = CandidateEvaluator.create('my-project') - actual = evaluator.get_best_candidate(candidates) + actual = evaluator.sort_best_candidate(candidates) assert actual is expected_best assert str(actual.version) == '3.0' @@ -489,7 +489,7 @@ def test_get_best_candidate__all_yanked(self, caplog): # Test a unicode string with a non-ascii character. (u'curly quote: \u2018', u'curly quote: \u2018'), ]) - def test_get_best_candidate__yanked_reason( + def test_sort_best_candidate__yanked_reason( self, caplog, yanked_reason, expected_reason, ): """ @@ -499,7 +499,7 @@ def test_get_best_candidate__yanked_reason( make_mock_candidate('1.0', yanked_reason=yanked_reason), ] evaluator = CandidateEvaluator.create('my-project') - actual = evaluator.get_best_candidate(candidates) + actual = evaluator.sort_best_candidate(candidates) assert str(actual.version) == '1.0' assert len(caplog.records) == 1 @@ -513,7 +513,9 @@ def test_get_best_candidate__yanked_reason( ) + expected_reason assert record.message == expected_message - def test_get_best_candidate__best_yanked_but_not_all(self, caplog): + def test_sort_best_candidate__best_yanked_but_not_all( + self, caplog, + ): """ Test the best candidates being yanked, but not all. """ @@ -526,7 +528,7 @@ def test_get_best_candidate__best_yanked_but_not_all(self, caplog): ] expected_best = candidates[1] evaluator = CandidateEvaluator.create('my-project') - actual = evaluator.get_best_candidate(candidates) + actual = evaluator.sort_best_candidate(candidates) assert actual is expected_best assert str(actual.version) == '2.0' diff --git a/tests/unit/test_unit_outdated.py b/tests/unit/test_unit_outdated.py index a5d37f81868..87ed16f0943 100644 --- a/tests/unit/test_unit_outdated.py +++ b/tests/unit/test_unit_outdated.py @@ -12,7 +12,7 @@ from pip._internal.utils import outdated -class MockFoundCandidates(object): +class MockBestCandidateResult(object): def __init__(self, best): self._best = best @@ -37,8 +37,8 @@ class MockPackageFinder(object): def create(cls, *args, **kwargs): return cls() - def find_candidates(self, project_name): - return MockFoundCandidates(self.INSTALLATION_CANDIDATES[0]) + def find_best_candidate(self, project_name): + return MockBestCandidateResult(self.INSTALLATION_CANDIDATES[0]) class MockDistribution(object): From 6554273fe5aa7f4acab8c893acfa230b15d4f745 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 19 Jul 2019 22:38:03 -0400 Subject: [PATCH 2/7] Pass the best candidate to BestCandidateResult instead of CandidateEvaluator. --- src/pip/_internal/index.py | 23 +++++++++-------------- src/pip/_internal/utils/outdated.py | 2 +- tests/unit/test_index.py | 3 ++- tests/unit/test_unit_outdated.py | 5 +---- 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 12f1071d414..78bc3dde4f1 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -656,10 +656,12 @@ def compute_best_candidate( """ applicable_candidates = self.get_applicable_candidates(candidates) + best_candidate = self.sort_best_candidate(applicable_candidates) + return BestCandidateResult( candidates, applicable_candidates=applicable_candidates, - evaluator=self, + best_candidate=best_candidate, ) def _sort_key(self, candidate): @@ -764,18 +766,19 @@ def __init__( self, candidates, # type: List[InstallationCandidate] applicable_candidates, # type: List[InstallationCandidate] - evaluator, # type: CandidateEvaluator + best_candidate, # type: Optional[InstallationCandidate] ): # type: (...) -> None """ :param candidates: A sequence of all available candidates found. :param applicable_candidates: The applicable candidates. - :param evaluator: A CandidateEvaluator object to sort applicable - candidates by order of preference. + :param best_candidate: The most preferred candidate found, or None + if no applicable candidates were found. """ self._applicable_candidates = applicable_candidates self._candidates = candidates - self._evaluator = evaluator + + self.best_candidate = best_candidate def iter_all(self): # type: () -> Iterable[InstallationCandidate] @@ -789,14 +792,6 @@ def iter_applicable(self): """ return iter(self._applicable_candidates) - def get_best(self): - # type: () -> Optional[InstallationCandidate] - """Return the best candidate available, or None if no applicable - candidates are found. - """ - candidates = list(self.iter_applicable()) - return self._evaluator.sort_best_candidate(candidates) - class PackageFinder(object): """This finds packages. @@ -1208,7 +1203,7 @@ def find_requirement(self, req, upgrade): best_candidate_result = self.find_best_candidate( req.name, specifier=req.specifier, hashes=hashes, ) - best_candidate = best_candidate_result.get_best() + best_candidate = best_candidate_result.best_candidate installed_version = None # type: Optional[_BaseVersion] if req.satisfied_by is not None: diff --git a/src/pip/_internal/utils/outdated.py b/src/pip/_internal/utils/outdated.py index 67971ad69c3..7f8ea3ca507 100644 --- a/src/pip/_internal/utils/outdated.py +++ b/src/pip/_internal/utils/outdated.py @@ -139,7 +139,7 @@ def pip_version_check(session, options): trusted_hosts=options.trusted_hosts, session=session, ) - best_candidate = finder.find_best_candidate("pip").get_best() + best_candidate = finder.find_best_candidate("pip").best_candidate if best_candidate is None: return pypi_version = str(best_candidate.version) diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index 79ec3f7b522..696c6810f95 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -400,7 +400,6 @@ def test_compute_best_candidate(self): result = evaluator.compute_best_candidate(candidates) assert result._candidates == candidates - assert result._evaluator is evaluator expected_applicable = candidates[:2] assert [str(c.version) for c in expected_applicable] == [ '1.10', @@ -408,6 +407,8 @@ def test_compute_best_candidate(self): ] assert result._applicable_candidates == expected_applicable + assert result.best_candidate is expected_applicable[1] + @pytest.mark.parametrize('hex_digest, expected', [ # Test a link with no hash. (None, 0), diff --git a/tests/unit/test_unit_outdated.py b/tests/unit/test_unit_outdated.py index 87ed16f0943..bdafc5fb775 100644 --- a/tests/unit/test_unit_outdated.py +++ b/tests/unit/test_unit_outdated.py @@ -14,10 +14,7 @@ class MockBestCandidateResult(object): def __init__(self, best): - self._best = best - - def get_best(self): - return self._best + self.best_candidate = best class MockPackageFinder(object): From 1a8dc9cda1be04b18389977e4d7e1998be3c799f Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 19 Jul 2019 22:40:07 -0400 Subject: [PATCH 3/7] Move compute_best_candidate() to the end of CandidateEvaluator. --- src/pip/_internal/index.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 78bc3dde4f1..df79ff13e3e 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -646,24 +646,6 @@ def get_applicable_candidates( project_name=self._project_name, ) - def compute_best_candidate( - self, - candidates, # type: List[InstallationCandidate] - ): - # type: (...) -> BestCandidateResult - """ - Compute and return a `BestCandidateResult` instance. - """ - applicable_candidates = self.get_applicable_candidates(candidates) - - best_candidate = self.sort_best_candidate(applicable_candidates) - - return BestCandidateResult( - candidates, - applicable_candidates=applicable_candidates, - best_candidate=best_candidate, - ) - def _sort_key(self, candidate): # type: (InstallationCandidate) -> CandidateSortingKey """ @@ -754,6 +736,24 @@ def sort_best_candidate( return best_candidate + def compute_best_candidate( + self, + candidates, # type: List[InstallationCandidate] + ): + # type: (...) -> BestCandidateResult + """ + Compute and return a `BestCandidateResult` instance. + """ + applicable_candidates = self.get_applicable_candidates(candidates) + + best_candidate = self.sort_best_candidate(applicable_candidates) + + return BestCandidateResult( + candidates, + applicable_candidates=applicable_candidates, + best_candidate=best_candidate, + ) + class BestCandidateResult(object): """A collection of candidates, returned by `PackageFinder.find_best_candidate`. From a644fb074fa2d4d3043406c37bde9805be96739c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 19 Jul 2019 22:40:59 -0400 Subject: [PATCH 4/7] Move BestCandidateResult before CandidateEvaluator. --- src/pip/_internal/index.py | 76 +++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index df79ff13e3e..2fa805555d6 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -545,6 +545,44 @@ def __init__( self.prefer_binary = prefer_binary +class BestCandidateResult(object): + """A collection of candidates, returned by `PackageFinder.find_best_candidate`. + + This class is only intended to be instantiated by CandidateEvaluator's + `compute_best_candidate()` method. + """ + + def __init__( + self, + candidates, # type: List[InstallationCandidate] + applicable_candidates, # type: List[InstallationCandidate] + best_candidate, # type: Optional[InstallationCandidate] + ): + # type: (...) -> None + """ + :param candidates: A sequence of all available candidates found. + :param applicable_candidates: The applicable candidates. + :param best_candidate: The most preferred candidate found, or None + if no applicable candidates were found. + """ + self._applicable_candidates = applicable_candidates + self._candidates = candidates + + self.best_candidate = best_candidate + + def iter_all(self): + # type: () -> Iterable[InstallationCandidate] + """Iterate through all candidates. + """ + return iter(self._candidates) + + def iter_applicable(self): + # type: () -> Iterable[InstallationCandidate] + """Iterate through the applicable candidates. + """ + return iter(self._applicable_candidates) + + class CandidateEvaluator(object): """ @@ -755,44 +793,6 @@ def compute_best_candidate( ) -class BestCandidateResult(object): - """A collection of candidates, returned by `PackageFinder.find_best_candidate`. - - This class is only intended to be instantiated by CandidateEvaluator's - `compute_best_candidate()` method. - """ - - def __init__( - self, - candidates, # type: List[InstallationCandidate] - applicable_candidates, # type: List[InstallationCandidate] - best_candidate, # type: Optional[InstallationCandidate] - ): - # type: (...) -> None - """ - :param candidates: A sequence of all available candidates found. - :param applicable_candidates: The applicable candidates. - :param best_candidate: The most preferred candidate found, or None - if no applicable candidates were found. - """ - self._applicable_candidates = applicable_candidates - self._candidates = candidates - - self.best_candidate = best_candidate - - def iter_all(self): - # type: () -> Iterable[InstallationCandidate] - """Iterate through all candidates. - """ - return iter(self._candidates) - - def iter_applicable(self): - # type: () -> Iterable[InstallationCandidate] - """Iterate through the applicable candidates. - """ - return iter(self._applicable_candidates) - - class PackageFinder(object): """This finds packages. From 3eb803aa26d4aa77ab2b1dc71efa77a6a4552a49 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 19 Jul 2019 22:45:54 -0400 Subject: [PATCH 5/7] Add some assertions to BestCandidateResult.__init__(). --- src/pip/_internal/index.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 2fa805555d6..881ffcc3763 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -565,6 +565,13 @@ def __init__( :param best_candidate: The most preferred candidate found, or None if no applicable candidates were found. """ + assert set(applicable_candidates) <= set(candidates) + + if best_candidate is None: + assert not applicable_candidates + else: + assert best_candidate in applicable_candidates + self._applicable_candidates = applicable_candidates self._candidates = candidates From 06d786dee1e0cd3423dad4f3c067a4be0c505178 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 24 Jul 2019 15:11:05 -0400 Subject: [PATCH 6/7] Add a test for compute_best_candidate() returning a None best candidate. --- tests/unit/test_index.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index 696c6810f95..a2b7752f737 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -409,6 +409,25 @@ def test_compute_best_candidate(self): assert result.best_candidate is expected_applicable[1] + def test_compute_best_candidate__none_best(self): + """ + Test returning a None best candidate. + """ + specifier = SpecifierSet('<= 1.10') + versions = ['1.11', '1.12'] + candidates = [ + make_mock_candidate(version) for version in versions + ] + evaluator = CandidateEvaluator.create( + 'my-project', + specifier=specifier, + ) + result = evaluator.compute_best_candidate(candidates) + + assert result._candidates == candidates + assert result._applicable_candidates == [] + assert result.best_candidate is None + @pytest.mark.parametrize('hex_digest, expected', [ # Test a link with no hash. (None, 0), From 8db3944a64787127b9a80e34bc30d2bb6489e52a Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Aug 2019 22:48:38 -0700 Subject: [PATCH 7/7] Add initial architecture section for index.py and PackageFinder. --- docs/html/development/architecture/index.rst | 1 + .../architecture/package-finding.rst | 202 ++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 docs/html/development/architecture/package-finding.rst diff --git a/docs/html/development/architecture/index.rst b/docs/html/development/architecture/index.rst index f3f221565c4..204604e8948 100644 --- a/docs/html/development/architecture/index.rst +++ b/docs/html/development/architecture/index.rst @@ -15,6 +15,7 @@ Architecture of pip's internals .. toctree:: :maxdepth: 2 + package-finding .. _`tracking issue`: https://github.com/pypa/pip/issues/6831 diff --git a/docs/html/development/architecture/package-finding.rst b/docs/html/development/architecture/package-finding.rst new file mode 100644 index 00000000000..1f17cb2c80e --- /dev/null +++ b/docs/html/development/architecture/package-finding.rst @@ -0,0 +1,202 @@ +Finding and choosing files (``index.py`` and ``PackageFinder``) +--------------------------------------------------------------- + +The ``index.py`` module is a top-level module in pip responsible for deciding +what file to download and from where, given a requirement for a project. The +module's functionality is largely exposed through and coordinated by the +module's ``PackageFinder`` class. + + +.. _index-py-overview: + +Overview +******** + +Here is a rough description of the process that pip uses to choose what +file to download for a package, given a requirement: + +1. Access the various network and file system locations configured for pip + that contain package files. These locations can include, for example, + pip's :ref:`--index-url <--index-url>` (with default + https://pypi.org/simple/ ) and any configured + :ref:`--extra-index-url <--extra-index-url>` locations. + Each of these locations is a `PEP 503`_ "simple repository" page, which + is an HTML page of anchor links. +2. Collect together all of the links (e.g. by parsing the anchor links + from the HTML pages) and create ``Link`` objects from each of these. +3. Determine which of the links are minimally relevant, using the + :ref:`LinkEvaluator ` class. Create an + ``InstallationCandidate`` object (aka candidate for install) for each + of these relevant links. +4. Further filter the collection of ``InstallationCandidate`` objects (using + the :ref:`CandidateEvaluator ` class) to a + collection of "applicable" candidates. +5. If there are applicable candidates, choose the best candidate by sorting + them (again using the :ref:`CandidateEvaluator + ` class). + +The remainder of this section is organized by documenting some of the +classes inside ``index.py``, in the following order: + +* the main :ref:`PackageFinder ` class, +* the :ref:`LinkEvaluator ` class, +* the :ref:`CandidateEvaluator ` class, +* the :ref:`CandidatePreferences ` class, and +* the :ref:`BestCandidateResult ` class. + + +.. _package-finder-class: + +The ``PackageFinder`` class +*************************** + +The ``PackageFinder`` class is the primary way through which code in pip +interacts with ``index.py``. It is an umbrella class that encapsulates and +groups together various package-finding functionality. + +The ``PackageFinder`` class is responsible for searching the network and file +system for what versions of a package pip can install, and also for deciding +which version is most preferred, given the user's preferences, target Python +environment, etc. + +The pip commands that use the ``PackageFinder`` class are: + +* :ref:`pip download` +* :ref:`pip install` +* :ref:`pip list` +* :ref:`pip wheel` + +The pip commands requiring use of the ``PackageFinder`` class generally +instantiate ``PackageFinder`` only once for the whole pip invocation. In +fact, pip creates this ``PackageFinder`` instance when command options +are first parsed. + +With the excepton of :ref:`pip list`, each of the above commands is +implemented as a ``Command`` class inheriting from ``RequirementCommand`` +(for example :ref:`pip download` is implemented by ``DownloadCommand``), and +the ``PackageFinder`` instance is created by calling the +``RequirementCommand`` class's ``_build_package_finder()`` method. ``pip +list``, on the other hand, constructs its ``PackageFinder`` instance by +calling the ``ListCommand`` class's ``_build_package_finder()``. (This +difference may simply be historical and may not actually be necessary.) + +Each of these commands also uses the ``PackageFinder`` class for pip's +"self-check," (i.e. to check whether a pip upgrade is available). In this +case, the ``PackageFinder`` instance is created by the ``outdated.py`` +module's ``pip_version_check()`` function. + +The ``PackageFinder`` class is responsible for doing all of the things listed +in the :ref:`Overview ` section like fetching and parsing +`PEP 503`_ simple repository HTML pages, evaluating which links in the simple +repository pages are relevant for each requirement, and further filtering and +sorting by preference the candidates for install coming from the relevant +links. + +One of ``PackageFinder``'s main top-level methods is +``find_best_candidate()``. This method does the following two things: + +1. Calls its ``find_all_candidates()`` method, which reads and parses all the + index URL's provided by the user, constructs a :ref:`LinkEvaluator + ` object to filter out some of those links, and then + returns a list of ``InstallationCandidates`` (aka candidates for install). + This corresponds to steps 1-3 of the :ref:`Overview ` + above. +2. Constructs a ``CandidateEvaluator`` object and uses that to determine + the best candidate. It does this by calling the ``CandidateEvaluator`` + class's ``compute_best_candidate()`` method on the return value of + ``find_all_candidates()``. This corresponds to steps 4-5 of the Overview. + + +.. _link-evaluator-class: + +The ``LinkEvaluator`` class +*************************** + +The ``LinkEvaluator`` class contains the business logic for determining +whether a link (e.g. in a simple repository page) satisfies minimal +conditions to be a candidate for install (resulting in an +``InstallationCandidate`` object). When making this determination, the +``LinkEvaluator`` instance uses information like the target Python +interpreter as well as user preferences like whether binary files are +allowed or preferred, etc. + +Specifically, the ``LinkEvaluator`` class has an ``evaluate_link()`` method +that returns whether a link is a candidate for install. + +Instances of this class are created by the ``PackageFinder`` class's +``make_link_evaluator()`` on a per-requirement basis. + + +.. _candidate-evaluator-class: + +The ``CandidateEvaluator`` class +******************************** + +The ``CandidateEvaluator`` class contains the business logic for evaluating +which ``InstallationCandidate`` objects should be preferred. This can be +viewed as a determination that is finer-grained than that performed by the +``LinkEvaluator`` class. + +In particular, the ``CandidateEvaluator`` class uses the whole set of +``InstallationCandidate`` objects when making its determinations, as opposed +to evaluating each candidate in isolation, as ``LinkEvaluator`` does. For +example, whether a pre-release is eligible for selection or whether a file +whose hash doesn't match is eligible depends on properties of the collection +as a whole. + +The ``CandidateEvaluator`` class uses information like the list of `PEP 425`_ +tags compatible with the target Python interpreter, hashes provided by the +user, and other user preferences, etc. + +Specifically, the class has a ``get_applicable_candidates()`` method. +This accepts the ``InstallationCandidate`` objects resulting from the links +accepted by the ``LinkEvaluator`` class's ``evaluate_link()`` method, and +it further filters them to a list of "applicable" candidates. + +The ``CandidateEvaluator`` class also has a ``sort_best_candidate()`` method +that orders the applicable candidates by preference, and then returns the +best (i.e. most preferred). + +Finally, the class has a ``compute_best_candidate()`` method that calls +``get_applicable_candidates()`` followed by ``sort_best_candidate()``, and +then returning a :ref:`BestCandidateResult ` +object encapsulating both the intermediate and final results of the decision. + +Instances of ``CandidateEvaluator`` are created by the ``PackageFinder`` +class's ``make_candidate_evaluator()`` method on a per-requirement basis. + + +.. _candidate-preferences-class: + +The ``CandidatePreferences`` class +********************************** + +The ``CandidatePreferences`` class is a simple container class that groups +together some of the user preferences that ``PackageFinder`` uses to +construct ``CandidateEvaluator`` objects (via the ``PackageFinder`` class's +``make_candidate_evaluator()`` method). + +A ``PackageFinder`` instance has a ``_candidate_prefs`` attribute whose value +is a ``CandidatePreferences`` instance. Since ``PackageFinder`` has a number +of responsibilities and options that control its behavior, grouping the +preferences specific to ``CandidateEvaluator`` helps maintainers know which +attributes are needed only for ``CandidateEvaluator``. + + +.. _best-candidate-result-class: + +The ``BestCandidateResult`` class +********************************* + +The ``BestCandidateResult`` class is a convenience "container" class that +encapsulates the result of finding the best candidate for a requirement. +(By "container" we mean an object that simply contains data and has no +business logic or state-changing methods of its own.) + +The class is the return type of both the ``CandidateEvaluator`` class's +``compute_best_candidate()`` method and the ``PackageFinder`` class's +``find_best_candidate()`` method. + + +.. _`PEP 425`: https://www.python.org/dev/peps/pep-0425/ +.. _`PEP 503`: https://www.python.org/dev/peps/pep-0503/