diff --git a/news/9615.feature.rst b/news/9615.feature.rst new file mode 100644 index 00000000000..075a6cd4295 --- /dev/null +++ b/news/9615.feature.rst @@ -0,0 +1 @@ +Explains why specified version cannot be retrieved when *Requires-Python* is not satisfied. diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 223d06df67e..2dfc03c36ed 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -222,9 +222,8 @@ def evaluate_link(self, link: Link) -> Tuple[bool, Optional[str]]: ignore_requires_python=self._ignore_requires_python, ) if not supports_python: - # Return None for the reason text to suppress calling - # _log_skipped_link(). - return (False, None) + reason = f"{version} Requires-Python {link.requires_python}" + return (False, reason) logger.debug("Found link %s, version: %s", link, version) @@ -610,7 +609,7 @@ def __init__( self.format_control = format_control # These are boring links that have already been logged somehow. - self._logged_links: Set[Link] = set() + self._logged_links: Set[Tuple[Link, str]] = set() # Don't include an allow_yanked default value to make sure each call # site considers whether yanked releases are allowed. This also causes @@ -690,6 +689,11 @@ def prefer_binary(self) -> bool: def set_prefer_binary(self) -> None: self._candidate_prefs.prefer_binary = True + def skipped_links_requires_python(self) -> List[str]: + return sorted( + {reason for _, reason in self._logged_links if "Requires-Python" in reason} + ) + def make_link_evaluator(self, project_name: str) -> LinkEvaluator: canonical_name = canonicalize_name(project_name) formats = self.format_control.get_allowed_formats(canonical_name) @@ -720,11 +724,11 @@ def _sort_links(self, links: Iterable[Link]) -> List[Link]: return no_eggs + eggs def _log_skipped_link(self, link: Link, reason: str) -> None: - if link not in self._logged_links: + if (link, reason) not in self._logged_links: # Put the link at the end so the reason is more visible and because # the link string is usually very long. logger.debug("Skipping link: %s: %s", reason, link) - self._logged_links.add(link) + self._logged_links.add((link, reason)) def get_install_candidate( self, link_evaluator: LinkEvaluator, link: Link @@ -735,8 +739,7 @@ def get_install_candidate( """ is_candidate, result = link_evaluator.evaluate_link(link) if not is_candidate: - if result: - self._log_skipped_link(link, reason=result) + self._log_skipped_link(link, result) return None return InstallationCandidate( diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 261d8d5605e..d3648e1581f 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -617,8 +617,15 @@ def _report_single_requirement_conflict( req_disp = f"{req} (from {parent.name})" cands = self._finder.find_all_candidates(req.project_name) + skips = self._finder.skipped_links_requires_python() versions = [str(v) for v in sorted({c.version for c in cands})] + if skips: + logger.critical( + "Ignored the following versions that require a different python " + "version: %s", + "; ".join(skips) or "none", + ) logger.critical( "Could not find a version that satisfies the requirement %s " "(from versions: %s)", diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index fa98f28c89c..7fbcd3766e8 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -120,7 +120,7 @@ class TestLinkEvaluator: [ ((3, 6, 5), False, (True, "1.12")), # Test an incompatible Python. - ((3, 6, 4), False, (False, None)), + ((3, 6, 4), False, (False, "1.12 Requires-Python == 3.6.5")), # Test an incompatible Python with ignore_requires_python=True. ((3, 6, 4), True, (True, "1.12")), ],