Skip to content

Commit e58a8a5

Browse files
authored
Merge pull request #11137 from sbidoul/download-info-sbi
Add download_info: DirectUrl to InstallRequirement
2 parents 713e00f + 05d2b85 commit e58a8a5

File tree

9 files changed

+259
-13
lines changed

9 files changed

+259
-13
lines changed

news/11137.feature.rst

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Record in wheel cache entries the URL of the original artifiact that was downloaded
2+
to build the cached wheels. The record is named ``origin.json`` and uses the PEP 610
3+
Direct URL format.

src/pip/_internal/cache.py

+25
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
import json
66
import logging
77
import os
8+
from pathlib import Path
89
from typing import Any, Dict, List, Optional, Set
910

1011
from pip._vendor.packaging.tags import Tag, interpreter_name, interpreter_version
1112
from pip._vendor.packaging.utils import canonicalize_name
1213

1314
from pip._internal.exceptions import InvalidWheelFilename
15+
from pip._internal.models.direct_url import DirectUrl
1416
from pip._internal.models.format_control import FormatControl
1517
from pip._internal.models.link import Link
1618
from pip._internal.models.wheel import Wheel
@@ -19,6 +21,8 @@
1921

2022
logger = logging.getLogger(__name__)
2123

24+
ORIGIN_JSON_NAME = "origin.json"
25+
2226

2327
def _hash_dict(d: Dict[str, str]) -> str:
2428
"""Return a stable sha224 of a dictionary."""
@@ -204,6 +208,10 @@ def __init__(
204208
):
205209
self.link = link
206210
self.persistent = persistent
211+
self.origin: Optional[DirectUrl] = None
212+
origin_direct_url_path = Path(self.link.file_path).parent / ORIGIN_JSON_NAME
213+
if origin_direct_url_path.exists():
214+
self.origin = DirectUrl.from_json(origin_direct_url_path.read_text())
207215

208216

209217
class WheelCache(Cache):
@@ -262,3 +270,20 @@ def get_cache_entry(
262270
return CacheEntry(retval, persistent=False)
263271

264272
return None
273+
274+
@staticmethod
275+
def record_download_origin(cache_dir: str, download_info: DirectUrl) -> None:
276+
origin_path = Path(cache_dir) / ORIGIN_JSON_NAME
277+
if origin_path.is_file():
278+
origin = DirectUrl.from_json(origin_path.read_text())
279+
# TODO: use DirectUrl.equivalent when https://github.com/pypa/pip/pull/10564
280+
# is merged.
281+
if origin.url != download_info.url:
282+
logger.warning(
283+
"Origin URL %s in cache entry %s does not match download URL %s. "
284+
"This is likely a pip bug or a cache corruption issue.",
285+
origin.url,
286+
cache_dir,
287+
download_info.url,
288+
)
289+
origin_path.write_text(download_info.to_json())

src/pip/_internal/operations/prepare.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
)
2626
from pip._internal.index.package_finder import PackageFinder
2727
from pip._internal.metadata import BaseDistribution
28+
from pip._internal.models.direct_url import ArchiveInfo
2829
from pip._internal.models.link import Link
2930
from pip._internal.models.wheel import Wheel
3031
from pip._internal.network.download import BatchDownloader, Downloader
@@ -35,9 +36,18 @@
3536
from pip._internal.network.session import PipSession
3637
from pip._internal.operations.build.build_tracker import BuildTracker
3738
from pip._internal.req.req_install import InstallRequirement
39+
from pip._internal.utils.direct_url_helpers import (
40+
direct_url_for_editable,
41+
direct_url_from_link,
42+
)
3843
from pip._internal.utils.hashes import Hashes, MissingHashes
3944
from pip._internal.utils.logging import indent_log
40-
from pip._internal.utils.misc import display_path, hide_url, is_installable_dir
45+
from pip._internal.utils.misc import (
46+
display_path,
47+
hash_file,
48+
hide_url,
49+
is_installable_dir,
50+
)
4151
from pip._internal.utils.temp_dir import TempDirectory
4252
from pip._internal.utils.unpacking import unpack_file
4353
from pip._internal.vcs import vcs
@@ -489,6 +499,23 @@ def _prepare_linked_requirement(
489499
hashes.check_against_path(file_path)
490500
local_file = File(file_path, content_type=None)
491501

502+
# If download_info is set, we got it from the wheel cache.
503+
if req.download_info is None:
504+
# Editables don't go through this function (see
505+
# prepare_editable_requirement).
506+
assert not req.editable
507+
req.download_info = direct_url_from_link(link, req.source_dir)
508+
# Make sure we have a hash in download_info. If we got it as part of the
509+
# URL, it will have been verified and we can rely on it. Otherwise we
510+
# compute it from the downloaded file.
511+
if (
512+
isinstance(req.download_info.info, ArchiveInfo)
513+
and not req.download_info.info.hash
514+
and local_file
515+
):
516+
hash = hash_file(local_file.path)[0].hexdigest()
517+
req.download_info.info.hash = f"sha256={hash}"
518+
492519
# For use in later processing,
493520
# preserve the file path on the requirement.
494521
if local_file:
@@ -547,6 +574,8 @@ def prepare_editable_requirement(
547574
)
548575
req.ensure_has_source_dir(self.src_dir)
549576
req.update_editable()
577+
assert req.source_dir
578+
req.download_info = direct_url_for_editable(req.unpacked_source_directory)
550579

551580
dist = _get_prepared_distribution(
552581
req,

src/pip/_internal/req/req_install.py

+6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
get_default_environment,
2727
get_directory_distribution,
2828
)
29+
from pip._internal.models.direct_url import DirectUrl
2930
from pip._internal.models.link import Link
3031
from pip._internal.operations.build.metadata import generate_metadata
3132
from pip._internal.operations.build.metadata_editable import generate_editable_metadata
@@ -112,6 +113,10 @@ def __init__(
112113
self.link = self.original_link = link
113114
self.original_link_is_in_wheel_cache = False
114115

116+
# Information about the location of the artifact that was downloaded . This
117+
# property is guaranteed to be set in resolver results.
118+
self.download_info: Optional[DirectUrl] = None
119+
115120
# Path to any downloaded or already-existing package.
116121
self.local_file_path: Optional[str] = None
117122
if self.link and self.link.is_file:
@@ -762,6 +767,7 @@ def install(
762767
if self.is_wheel:
763768
assert self.local_file_path
764769
direct_url = None
770+
# TODO this can be refactored to direct_url = self.download_info
765771
if self.editable:
766772
direct_url = direct_url_for_editable(self.unpacked_source_directory)
767773
elif self.original_link:

src/pip/_internal/resolution/legacy/resolver.py

+9
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from pip._internal.resolution.base import BaseResolver, InstallRequirementProvider
4646
from pip._internal.utils import compatibility_tags
4747
from pip._internal.utils.compatibility_tags import get_supported
48+
from pip._internal.utils.direct_url_helpers import direct_url_from_link
4849
from pip._internal.utils.logging import indent_log
4950
from pip._internal.utils.misc import normalize_version_info
5051
from pip._internal.utils.packaging import check_requires_python
@@ -431,6 +432,14 @@ def _populate_link(self, req: InstallRequirement) -> None:
431432
logger.debug("Using cached wheel link: %s", cache_entry.link)
432433
if req.link is req.original_link and cache_entry.persistent:
433434
req.original_link_is_in_wheel_cache = True
435+
if cache_entry.origin is not None:
436+
req.download_info = cache_entry.origin
437+
else:
438+
# Legacy cache entry that does not have origin.json.
439+
# download_info may miss the archive_info.hash field.
440+
req.download_info = direct_url_from_link(
441+
req.link, link_is_in_wheel_cache=cache_entry.persistent
442+
)
434443
req.link = cache_entry.link
435444

436445
def _get_dist_for(self, req: InstallRequirement) -> BaseDistribution:

src/pip/_internal/resolution/resolvelib/candidates.py

+12-6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
install_req_from_line,
1919
)
2020
from pip._internal.req.req_install import InstallRequirement
21+
from pip._internal.utils.direct_url_helpers import direct_url_from_link
2122
from pip._internal.utils.misc import normalize_version_info
2223

2324
from .base import Candidate, CandidateVersion, Requirement, format_name
@@ -281,12 +282,17 @@ def __init__(
281282
version, wheel_version, name
282283
)
283284

284-
if (
285-
cache_entry is not None
286-
and cache_entry.persistent
287-
and template.link is template.original_link
288-
):
289-
ireq.original_link_is_in_wheel_cache = True
285+
if cache_entry is not None:
286+
if cache_entry.persistent and template.link is template.original_link:
287+
ireq.original_link_is_in_wheel_cache = True
288+
if cache_entry.origin is not None:
289+
ireq.download_info = cache_entry.origin
290+
else:
291+
# Legacy cache entry that does not have origin.json.
292+
# download_info may miss the archive_info.hash field.
293+
ireq.download_info = direct_url_from_link(
294+
source_link, link_is_in_wheel_cache=cache_entry.persistent
295+
)
290296

291297
super().__init__(
292298
link=link,

src/pip/_internal/wheel_builder.py

+6
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,12 @@ def build(
354354
req.editable and req.permit_editable_wheels,
355355
)
356356
if wheel_file:
357+
# Record the download origin in the cache
358+
if req.download_info is not None:
359+
# download_info is guaranteed to be set because when we build an
360+
# InstallRequirement it has been through the preparer before, but
361+
# let's be cautious.
362+
wheel_cache.record_download_origin(cache_dir, req.download_info)
357363
# Update the link for this.
358364
req.link = Link(path_to_url(wheel_file))
359365
req.local_file_path = req.link.file_path

tests/functional/test_install.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1550,9 +1550,9 @@ def test_install_builds_wheels(script: PipTestEnvironment, data: TestData) -> No
15501550
)
15511551
# Must have installed it all
15521552
assert expected in str(res), str(res)
1553-
wheels = []
1553+
wheels: List[str] = []
15541554
for _, _, files in os.walk(wheels_cache):
1555-
wheels.extend(files)
1555+
wheels.extend(f for f in files if f.endswith(".whl"))
15561556
# and built wheels for upper and wheelbroken
15571557
assert "Building wheel for upper" in str(res), str(res)
15581558
assert "Building wheel for wheelb" in str(res), str(res)

0 commit comments

Comments
 (0)