diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 2ec258c1231..f768f0f14ab 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -37,6 +37,7 @@ from pip._internal.utils.misc import build_netloc from pip._internal.utils.packaging import check_requires_python from pip._internal.utils.unpacking import SUPPORTED_EXTENSIONS +from pip._internal.utils.urls import path_to_url from pip._internal.utils.variant import ( VariantJson, get_cached_variant_hashes_by_priority, @@ -930,6 +931,14 @@ def find_all_candidates(self, project_name: str) -> List[InstallationCandidate]: ) page_candidates = list(page_candidates_it) + # Since candidates_from_page does not get used to process file sources + # from find-links, manually inject evaluate_links calls here. + _links = [ + Link(path_to_url(fl.variants_json)) + for fl in collected_sources.find_links + if fl.variants_json is not None + ] + self.evaluate_links(link_evaluator, _links) file_links_it = itertools.chain.from_iterable( source.file_links() for sources in collected_sources diff --git a/src/pip/_internal/index/sources.py b/src/pip/_internal/index/sources.py index 3dafb30e6eb..5ffea4e12a9 100644 --- a/src/pip/_internal/index/sources.py +++ b/src/pip/_internal/index/sources.py @@ -51,6 +51,7 @@ def __init__(self, path: str) -> None: self._path = path self._page_candidates: List[str] = [] self._project_name_to_urls: Dict[str, List[str]] = defaultdict(list) + self.variants_json = None self._scanned_directory = False def _scan_directory(self) -> None: @@ -71,6 +72,8 @@ def _scan_directory(self) -> None: try: project_filename = parse_sdist_filename(entry.name)[0] except InvalidSdistFilename: + if entry.name.endswith("-variants.json"): + self.variants_json = entry.path continue self._project_name_to_urls[project_filename].append(url) @@ -130,6 +133,10 @@ def file_links(self) -> FoundLinks: for url in self._path_to_urls.project_name_to_urls[self._project_name]: yield Link(url) + @property + def variants_json(self): + return self._path_to_urls.variants_json + class _LocalFileSource(LinkSource): """``--find-links=`` or ``--[extra-]index-url=``. diff --git a/src/pip/_internal/utils/variant.py b/src/pip/_internal/utils/variant.py index af6fe6b9c87..83ea4afd236 100644 --- a/src/pip/_internal/utils/variant.py +++ b/src/pip/_internal/utils/variant.py @@ -49,7 +49,7 @@ def __hash__(self) -> int: def get_variants_json_filename(wheel: Wheel) -> str: # these are normalized, but with .replace("_", "-") return ( - f"{wheel.name.replace("-", "_")}-{wheel.version.replace("-", "_")}-" + f"{wheel.name.replace('-', '_')}-{wheel.version.replace('-', '_')}-" "variants.json" ) diff --git a/src/pip/_vendor/packaging/utils.py b/src/pip/_vendor/packaging/utils.py index 23450953df7..62387ed21f6 100644 --- a/src/pip/_vendor/packaging/utils.py +++ b/src/pip/_vendor/packaging/utils.py @@ -101,7 +101,7 @@ def parse_wheel_filename( filename = filename[:-4] dashes = filename.count("-") - if dashes not in (4, 5): + if dashes not in (4, 5, 6): raise InvalidWheelFilename( f"Invalid wheel filename (wrong number of parts): {filename!r}" ) @@ -120,14 +120,22 @@ def parse_wheel_filename( f"Invalid wheel filename (invalid version): {filename!r}" ) from e - if dashes == 5: + if dashes in (5, 6): build_part = parts[2] build_match = _build_tag_regex.match(build_part) if build_match is None: - raise InvalidWheelFilename( - f"Invalid build number: {build_part} in {filename!r}" - ) - build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2))) + # When there are 5 dashes, not matching the build number could be OK because + # we could have a variant tag. + variant_hash_pattern = r'[a-zA-Z0-9]{8}' + possible_variant_hash = parts[-1].split("-")[-1] + if dashes == 6 or (dashes == 5 and not re.match(variant_hash_pattern, possible_variant_hash)): + raise InvalidWheelFilename( + f"Invalid build number: {build_part} in {filename!r}" + ) + else: + build = () + else: + build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2))) else: build = () tags = parse_tag(parts[-1])