Path: blob/main/test/lib/python3.9/site-packages/pip/_internal/cache.py
4799 views
"""Cache Management1"""23import hashlib4import json5import logging6import os7from typing import Any, Dict, List, Optional, Set89from pip._vendor.packaging.tags import Tag, interpreter_name, interpreter_version10from pip._vendor.packaging.utils import canonicalize_name1112from pip._internal.exceptions import InvalidWheelFilename13from pip._internal.models.format_control import FormatControl14from pip._internal.models.link import Link15from pip._internal.models.wheel import Wheel16from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds17from pip._internal.utils.urls import path_to_url1819logger = logging.getLogger(__name__)202122def _hash_dict(d: Dict[str, str]) -> str:23"""Return a stable sha224 of a dictionary."""24s = json.dumps(d, sort_keys=True, separators=(",", ":"), ensure_ascii=True)25return hashlib.sha224(s.encode("ascii")).hexdigest()262728class Cache:29"""An abstract class - provides cache directories for data from links303132:param cache_dir: The root of the cache.33:param format_control: An object of FormatControl class to limit34binaries being read from the cache.35:param allowed_formats: which formats of files the cache should store.36('binary' and 'source' are the only allowed values)37"""3839def __init__(40self, cache_dir: str, format_control: FormatControl, allowed_formats: Set[str]41) -> None:42super().__init__()43assert not cache_dir or os.path.isabs(cache_dir)44self.cache_dir = cache_dir or None45self.format_control = format_control46self.allowed_formats = allowed_formats4748_valid_formats = {"source", "binary"}49assert self.allowed_formats.union(_valid_formats) == _valid_formats5051def _get_cache_path_parts(self, link: Link) -> List[str]:52"""Get parts of part that must be os.path.joined with cache_dir"""5354# We want to generate an url to use as our cache key, we don't want to55# just re-use the URL because it might have other items in the fragment56# and we don't care about those.57key_parts = {"url": link.url_without_fragment}58if link.hash_name is not None and link.hash is not None:59key_parts[link.hash_name] = link.hash60if link.subdirectory_fragment:61key_parts["subdirectory"] = link.subdirectory_fragment6263# Include interpreter name, major and minor version in cache key64# to cope with ill-behaved sdists that build a different wheel65# depending on the python version their setup.py is being run on,66# and don't encode the difference in compatibility tags.67# https://github.com/pypa/pip/issues/729668key_parts["interpreter_name"] = interpreter_name()69key_parts["interpreter_version"] = interpreter_version()7071# Encode our key url with sha224, we'll use this because it has similar72# security properties to sha256, but with a shorter total output (and73# thus less secure). However the differences don't make a lot of74# difference for our use case here.75hashed = _hash_dict(key_parts)7677# We want to nest the directories some to prevent having a ton of top78# level directories where we might run out of sub directories on some79# FS.80parts = [hashed[:2], hashed[2:4], hashed[4:6], hashed[6:]]8182return parts8384def _get_candidates(self, link: Link, canonical_package_name: str) -> List[Any]:85can_not_cache = not self.cache_dir or not canonical_package_name or not link86if can_not_cache:87return []8889formats = self.format_control.get_allowed_formats(canonical_package_name)90if not self.allowed_formats.intersection(formats):91return []9293candidates = []94path = self.get_path_for_link(link)95if os.path.isdir(path):96for candidate in os.listdir(path):97candidates.append((candidate, path))98return candidates99100def get_path_for_link(self, link: Link) -> str:101"""Return a directory to store cached items in for link."""102raise NotImplementedError()103104def get(105self,106link: Link,107package_name: Optional[str],108supported_tags: List[Tag],109) -> Link:110"""Returns a link to a cached item if it exists, otherwise returns the111passed link.112"""113raise NotImplementedError()114115116class SimpleWheelCache(Cache):117"""A cache of wheels for future installs."""118119def __init__(self, cache_dir: str, format_control: FormatControl) -> None:120super().__init__(cache_dir, format_control, {"binary"})121122def get_path_for_link(self, link: Link) -> str:123"""Return a directory to store cached wheels for link124125Because there are M wheels for any one sdist, we provide a directory126to cache them in, and then consult that directory when looking up127cache hits.128129We only insert things into the cache if they have plausible version130numbers, so that we don't contaminate the cache with things that were131not unique. E.g. ./package might have dozens of installs done for it132and build a version of 0.0...and if we built and cached a wheel, we'd133end up using the same wheel even if the source has been edited.134135:param link: The link of the sdist for which this will cache wheels.136"""137parts = self._get_cache_path_parts(link)138assert self.cache_dir139# Store wheels within the root cache_dir140return os.path.join(self.cache_dir, "wheels", *parts)141142def get(143self,144link: Link,145package_name: Optional[str],146supported_tags: List[Tag],147) -> Link:148candidates = []149150if not package_name:151return link152153canonical_package_name = canonicalize_name(package_name)154for wheel_name, wheel_dir in self._get_candidates(link, canonical_package_name):155try:156wheel = Wheel(wheel_name)157except InvalidWheelFilename:158continue159if canonicalize_name(wheel.name) != canonical_package_name:160logger.debug(161"Ignoring cached wheel %s for %s as it "162"does not match the expected distribution name %s.",163wheel_name,164link,165package_name,166)167continue168if not wheel.supported(supported_tags):169# Built for a different python/arch/etc170continue171candidates.append(172(173wheel.support_index_min(supported_tags),174wheel_name,175wheel_dir,176)177)178179if not candidates:180return link181182_, wheel_name, wheel_dir = min(candidates)183return Link(path_to_url(os.path.join(wheel_dir, wheel_name)))184185186class EphemWheelCache(SimpleWheelCache):187"""A SimpleWheelCache that creates it's own temporary cache directory"""188189def __init__(self, format_control: FormatControl) -> None:190self._temp_dir = TempDirectory(191kind=tempdir_kinds.EPHEM_WHEEL_CACHE,192globally_managed=True,193)194195super().__init__(self._temp_dir.path, format_control)196197198class CacheEntry:199def __init__(200self,201link: Link,202persistent: bool,203):204self.link = link205self.persistent = persistent206207208class WheelCache(Cache):209"""Wraps EphemWheelCache and SimpleWheelCache into a single Cache210211This Cache allows for gracefully degradation, using the ephem wheel cache212when a certain link is not found in the simple wheel cache first.213"""214215def __init__(self, cache_dir: str, format_control: FormatControl) -> None:216super().__init__(cache_dir, format_control, {"binary"})217self._wheel_cache = SimpleWheelCache(cache_dir, format_control)218self._ephem_cache = EphemWheelCache(format_control)219220def get_path_for_link(self, link: Link) -> str:221return self._wheel_cache.get_path_for_link(link)222223def get_ephem_path_for_link(self, link: Link) -> str:224return self._ephem_cache.get_path_for_link(link)225226def get(227self,228link: Link,229package_name: Optional[str],230supported_tags: List[Tag],231) -> Link:232cache_entry = self.get_cache_entry(link, package_name, supported_tags)233if cache_entry is None:234return link235return cache_entry.link236237def get_cache_entry(238self,239link: Link,240package_name: Optional[str],241supported_tags: List[Tag],242) -> Optional[CacheEntry]:243"""Returns a CacheEntry with a link to a cached item if it exists or244None. The cache entry indicates if the item was found in the persistent245or ephemeral cache.246"""247retval = self._wheel_cache.get(248link=link,249package_name=package_name,250supported_tags=supported_tags,251)252if retval is not link:253return CacheEntry(retval, persistent=True)254255retval = self._ephem_cache.get(256link=link,257package_name=package_name,258supported_tags=supported_tags,259)260if retval is not link:261return CacheEntry(retval, persistent=False)262263return None264265266