Path: blob/main/test/lib/python3.9/site-packages/pip/_internal/operations/prepare.py
4804 views
"""Prepares a distribution for installation1"""23# The following comment should be removed at some point in the future.4# mypy: strict-optional=False56import logging7import mimetypes8import os9import shutil10from typing import Dict, Iterable, List, Optional1112from pip._vendor.packaging.utils import canonicalize_name1314from pip._internal.distributions import make_distribution_for_install_requirement15from pip._internal.distributions.installed import InstalledDistribution16from pip._internal.exceptions import (17DirectoryUrlHashUnsupported,18HashMismatch,19HashUnpinned,20InstallationError,21NetworkConnectionError,22PreviousBuildDirError,23VcsHashUnsupported,24)25from pip._internal.index.package_finder import PackageFinder26from pip._internal.metadata import BaseDistribution27from pip._internal.models.link import Link28from pip._internal.models.wheel import Wheel29from pip._internal.network.download import BatchDownloader, Downloader30from pip._internal.network.lazy_wheel import (31HTTPRangeRequestUnsupported,32dist_from_wheel_url,33)34from pip._internal.network.session import PipSession35from pip._internal.operations.build.build_tracker import BuildTracker36from pip._internal.req.req_install import InstallRequirement37from pip._internal.utils.hashes import Hashes, MissingHashes38from pip._internal.utils.logging import indent_log39from pip._internal.utils.misc import display_path, hide_url, is_installable_dir40from pip._internal.utils.temp_dir import TempDirectory41from pip._internal.utils.unpacking import unpack_file42from pip._internal.vcs import vcs4344logger = logging.getLogger(__name__)454647def _get_prepared_distribution(48req: InstallRequirement,49build_tracker: BuildTracker,50finder: PackageFinder,51build_isolation: bool,52check_build_deps: bool,53) -> BaseDistribution:54"""Prepare a distribution for installation."""55abstract_dist = make_distribution_for_install_requirement(req)56with build_tracker.track(req):57abstract_dist.prepare_distribution_metadata(58finder, build_isolation, check_build_deps59)60return abstract_dist.get_metadata_distribution()616263def unpack_vcs_link(link: Link, location: str, verbosity: int) -> None:64vcs_backend = vcs.get_backend_for_scheme(link.scheme)65assert vcs_backend is not None66vcs_backend.unpack(location, url=hide_url(link.url), verbosity=verbosity)676869class File:70def __init__(self, path: str, content_type: Optional[str]) -> None:71self.path = path72if content_type is None:73self.content_type = mimetypes.guess_type(path)[0]74else:75self.content_type = content_type767778def get_http_url(79link: Link,80download: Downloader,81download_dir: Optional[str] = None,82hashes: Optional[Hashes] = None,83) -> File:84temp_dir = TempDirectory(kind="unpack", globally_managed=True)85# If a download dir is specified, is the file already downloaded there?86already_downloaded_path = None87if download_dir:88already_downloaded_path = _check_download_dir(link, download_dir, hashes)8990if already_downloaded_path:91from_path = already_downloaded_path92content_type = None93else:94# let's download to a tmp dir95from_path, content_type = download(link, temp_dir.path)96if hashes:97hashes.check_against_path(from_path)9899return File(from_path, content_type)100101102def get_file_url(103link: Link, download_dir: Optional[str] = None, hashes: Optional[Hashes] = None104) -> File:105"""Get file and optionally check its hash."""106# If a download dir is specified, is the file already there and valid?107already_downloaded_path = None108if download_dir:109already_downloaded_path = _check_download_dir(link, download_dir, hashes)110111if already_downloaded_path:112from_path = already_downloaded_path113else:114from_path = link.file_path115116# If --require-hashes is off, `hashes` is either empty, the117# link's embedded hash, or MissingHashes; it is required to118# match. If --require-hashes is on, we are satisfied by any119# hash in `hashes` matching: a URL-based or an option-based120# one; no internet-sourced hash will be in `hashes`.121if hashes:122hashes.check_against_path(from_path)123return File(from_path, None)124125126def unpack_url(127link: Link,128location: str,129download: Downloader,130verbosity: int,131download_dir: Optional[str] = None,132hashes: Optional[Hashes] = None,133) -> Optional[File]:134"""Unpack link into location, downloading if required.135136:param hashes: A Hashes object, one of whose embedded hashes must match,137or HashMismatch will be raised. If the Hashes is empty, no matches are138required, and unhashable types of requirements (like VCS ones, which139would ordinarily raise HashUnsupported) are allowed.140"""141# non-editable vcs urls142if link.is_vcs:143unpack_vcs_link(link, location, verbosity=verbosity)144return None145146assert not link.is_existing_dir()147148# file urls149if link.is_file:150file = get_file_url(link, download_dir, hashes=hashes)151152# http urls153else:154file = get_http_url(155link,156download,157download_dir,158hashes=hashes,159)160161# unpack the archive to the build dir location. even when only downloading162# archives, they have to be unpacked to parse dependencies, except wheels163if not link.is_wheel:164unpack_file(file.path, location, file.content_type)165166return file167168169def _check_download_dir(170link: Link, download_dir: str, hashes: Optional[Hashes]171) -> Optional[str]:172"""Check download_dir for previously downloaded file with correct hash173If a correct file is found return its path else None174"""175download_path = os.path.join(download_dir, link.filename)176177if not os.path.exists(download_path):178return None179180# If already downloaded, does its hash match?181logger.info("File was already downloaded %s", download_path)182if hashes:183try:184hashes.check_against_path(download_path)185except HashMismatch:186logger.warning(187"Previously-downloaded file %s has bad hash. Re-downloading.",188download_path,189)190os.unlink(download_path)191return None192return download_path193194195class RequirementPreparer:196"""Prepares a Requirement"""197198def __init__(199self,200build_dir: str,201download_dir: Optional[str],202src_dir: str,203build_isolation: bool,204check_build_deps: bool,205build_tracker: BuildTracker,206session: PipSession,207progress_bar: str,208finder: PackageFinder,209require_hashes: bool,210use_user_site: bool,211lazy_wheel: bool,212verbosity: int,213) -> None:214super().__init__()215216self.src_dir = src_dir217self.build_dir = build_dir218self.build_tracker = build_tracker219self._session = session220self._download = Downloader(session, progress_bar)221self._batch_download = BatchDownloader(session, progress_bar)222self.finder = finder223224# Where still-packed archives should be written to. If None, they are225# not saved, and are deleted immediately after unpacking.226self.download_dir = download_dir227228# Is build isolation allowed?229self.build_isolation = build_isolation230231# Should check build dependencies?232self.check_build_deps = check_build_deps233234# Should hash-checking be required?235self.require_hashes = require_hashes236237# Should install in user site-packages?238self.use_user_site = use_user_site239240# Should wheels be downloaded lazily?241self.use_lazy_wheel = lazy_wheel242243# How verbose should underlying tooling be?244self.verbosity = verbosity245246# Memoized downloaded files, as mapping of url: path.247self._downloaded: Dict[str, str] = {}248249# Previous "header" printed for a link-based InstallRequirement250self._previous_requirement_header = ("", "")251252def _log_preparing_link(self, req: InstallRequirement) -> None:253"""Provide context for the requirement being prepared."""254if req.link.is_file and not req.original_link_is_in_wheel_cache:255message = "Processing %s"256information = str(display_path(req.link.file_path))257else:258message = "Collecting %s"259information = str(req.req or req)260261if (message, information) != self._previous_requirement_header:262self._previous_requirement_header = (message, information)263logger.info(message, information)264265if req.original_link_is_in_wheel_cache:266with indent_log():267logger.info("Using cached %s", req.link.filename)268269def _ensure_link_req_src_dir(270self, req: InstallRequirement, parallel_builds: bool271) -> None:272"""Ensure source_dir of a linked InstallRequirement."""273# Since source_dir is only set for editable requirements.274if req.link.is_wheel:275# We don't need to unpack wheels, so no need for a source276# directory.277return278assert req.source_dir is None279if req.link.is_existing_dir():280# build local directories in-tree281req.source_dir = req.link.file_path282return283284# We always delete unpacked sdists after pip runs.285req.ensure_has_source_dir(286self.build_dir,287autodelete=True,288parallel_builds=parallel_builds,289)290291# If a checkout exists, it's unwise to keep going. version292# inconsistencies are logged later, but do not fail the293# installation.294# FIXME: this won't upgrade when there's an existing295# package unpacked in `req.source_dir`296# TODO: this check is now probably dead code297if is_installable_dir(req.source_dir):298raise PreviousBuildDirError(299"pip can't proceed with requirements '{}' due to a"300"pre-existing build directory ({}). This is likely "301"due to a previous installation that failed . pip is "302"being responsible and not assuming it can delete this. "303"Please delete it and try again.".format(req, req.source_dir)304)305306def _get_linked_req_hashes(self, req: InstallRequirement) -> Hashes:307# By the time this is called, the requirement's link should have308# been checked so we can tell what kind of requirements req is309# and raise some more informative errors than otherwise.310# (For example, we can raise VcsHashUnsupported for a VCS URL311# rather than HashMissing.)312if not self.require_hashes:313return req.hashes(trust_internet=True)314315# We could check these first 2 conditions inside unpack_url316# and save repetition of conditions, but then we would317# report less-useful error messages for unhashable318# requirements, complaining that there's no hash provided.319if req.link.is_vcs:320raise VcsHashUnsupported()321if req.link.is_existing_dir():322raise DirectoryUrlHashUnsupported()323324# Unpinned packages are asking for trouble when a new version325# is uploaded. This isn't a security check, but it saves users326# a surprising hash mismatch in the future.327# file:/// URLs aren't pinnable, so don't complain about them328# not being pinned.329if req.original_link is None and not req.is_pinned:330raise HashUnpinned()331332# If known-good hashes are missing for this requirement,333# shim it with a facade object that will provoke hash334# computation and then raise a HashMissing exception335# showing the user what the hash should be.336return req.hashes(trust_internet=False) or MissingHashes()337338def _fetch_metadata_using_lazy_wheel(339self,340link: Link,341) -> Optional[BaseDistribution]:342"""Fetch metadata using lazy wheel, if possible."""343if not self.use_lazy_wheel:344return None345if self.require_hashes:346logger.debug("Lazy wheel is not used as hash checking is required")347return None348if link.is_file or not link.is_wheel:349logger.debug(350"Lazy wheel is not used as %r does not points to a remote wheel",351link,352)353return None354355wheel = Wheel(link.filename)356name = canonicalize_name(wheel.name)357logger.info(358"Obtaining dependency information from %s %s",359name,360wheel.version,361)362url = link.url.split("#", 1)[0]363try:364return dist_from_wheel_url(name, url, self._session)365except HTTPRangeRequestUnsupported:366logger.debug("%s does not support range requests", url)367return None368369def _complete_partial_requirements(370self,371partially_downloaded_reqs: Iterable[InstallRequirement],372parallel_builds: bool = False,373) -> None:374"""Download any requirements which were only fetched by metadata."""375# Download to a temporary directory. These will be copied over as376# needed for downstream 'download', 'wheel', and 'install' commands.377temp_dir = TempDirectory(kind="unpack", globally_managed=True).path378379# Map each link to the requirement that owns it. This allows us to set380# `req.local_file_path` on the appropriate requirement after passing381# all the links at once into BatchDownloader.382links_to_fully_download: Dict[Link, InstallRequirement] = {}383for req in partially_downloaded_reqs:384assert req.link385links_to_fully_download[req.link] = req386387batch_download = self._batch_download(388links_to_fully_download.keys(),389temp_dir,390)391for link, (filepath, _) in batch_download:392logger.debug("Downloading link %s to %s", link, filepath)393req = links_to_fully_download[link]394req.local_file_path = filepath395396# This step is necessary to ensure all lazy wheels are processed397# successfully by the 'download', 'wheel', and 'install' commands.398for req in partially_downloaded_reqs:399self._prepare_linked_requirement(req, parallel_builds)400401def prepare_linked_requirement(402self, req: InstallRequirement, parallel_builds: bool = False403) -> BaseDistribution:404"""Prepare a requirement to be obtained from req.link."""405assert req.link406link = req.link407self._log_preparing_link(req)408with indent_log():409# Check if the relevant file is already available410# in the download directory411file_path = None412if self.download_dir is not None and link.is_wheel:413hashes = self._get_linked_req_hashes(req)414file_path = _check_download_dir(req.link, self.download_dir, hashes)415416if file_path is not None:417# The file is already available, so mark it as downloaded418self._downloaded[req.link.url] = file_path419else:420# The file is not available, attempt to fetch only metadata421wheel_dist = self._fetch_metadata_using_lazy_wheel(link)422if wheel_dist is not None:423req.needs_more_preparation = True424return wheel_dist425426# None of the optimizations worked, fully prepare the requirement427return self._prepare_linked_requirement(req, parallel_builds)428429def prepare_linked_requirements_more(430self, reqs: Iterable[InstallRequirement], parallel_builds: bool = False431) -> None:432"""Prepare linked requirements more, if needed."""433reqs = [req for req in reqs if req.needs_more_preparation]434for req in reqs:435# Determine if any of these requirements were already downloaded.436if self.download_dir is not None and req.link.is_wheel:437hashes = self._get_linked_req_hashes(req)438file_path = _check_download_dir(req.link, self.download_dir, hashes)439if file_path is not None:440self._downloaded[req.link.url] = file_path441req.needs_more_preparation = False442443# Prepare requirements we found were already downloaded for some444# reason. The other downloads will be completed separately.445partially_downloaded_reqs: List[InstallRequirement] = []446for req in reqs:447if req.needs_more_preparation:448partially_downloaded_reqs.append(req)449else:450self._prepare_linked_requirement(req, parallel_builds)451452# TODO: separate this part out from RequirementPreparer when the v1453# resolver can be removed!454self._complete_partial_requirements(455partially_downloaded_reqs,456parallel_builds=parallel_builds,457)458459def _prepare_linked_requirement(460self, req: InstallRequirement, parallel_builds: bool461) -> BaseDistribution:462assert req.link463link = req.link464465self._ensure_link_req_src_dir(req, parallel_builds)466hashes = self._get_linked_req_hashes(req)467468if link.is_existing_dir():469local_file = None470elif link.url not in self._downloaded:471try:472local_file = unpack_url(473link,474req.source_dir,475self._download,476self.verbosity,477self.download_dir,478hashes,479)480except NetworkConnectionError as exc:481raise InstallationError(482"Could not install requirement {} because of HTTP "483"error {} for URL {}".format(req, exc, link)484)485else:486file_path = self._downloaded[link.url]487if hashes:488hashes.check_against_path(file_path)489local_file = File(file_path, content_type=None)490491# For use in later processing,492# preserve the file path on the requirement.493if local_file:494req.local_file_path = local_file.path495496dist = _get_prepared_distribution(497req,498self.build_tracker,499self.finder,500self.build_isolation,501self.check_build_deps,502)503return dist504505def save_linked_requirement(self, req: InstallRequirement) -> None:506assert self.download_dir is not None507assert req.link is not None508link = req.link509if link.is_vcs or (link.is_existing_dir() and req.editable):510# Make a .zip of the source_dir we already created.511req.archive(self.download_dir)512return513514if link.is_existing_dir():515logger.debug(516"Not copying link to destination directory "517"since it is a directory: %s",518link,519)520return521if req.local_file_path is None:522# No distribution was downloaded for this requirement.523return524525download_location = os.path.join(self.download_dir, link.filename)526if not os.path.exists(download_location):527shutil.copy(req.local_file_path, download_location)528download_path = display_path(download_location)529logger.info("Saved %s", download_path)530531def prepare_editable_requirement(532self,533req: InstallRequirement,534) -> BaseDistribution:535"""Prepare an editable requirement."""536assert req.editable, "cannot prepare a non-editable req as editable"537538logger.info("Obtaining %s", req)539540with indent_log():541if self.require_hashes:542raise InstallationError(543"The editable requirement {} cannot be installed when "544"requiring hashes, because there is no single file to "545"hash.".format(req)546)547req.ensure_has_source_dir(self.src_dir)548req.update_editable()549550dist = _get_prepared_distribution(551req,552self.build_tracker,553self.finder,554self.build_isolation,555self.check_build_deps,556)557558req.check_if_exists(self.use_user_site)559560return dist561562def prepare_installed_requirement(563self,564req: InstallRequirement,565skip_reason: str,566) -> BaseDistribution:567"""Prepare an already-installed requirement."""568assert req.satisfied_by, "req should have been satisfied but isn't"569assert skip_reason is not None, (570"did not get skip reason skipped but req.satisfied_by "571"is set to {}".format(req.satisfied_by)572)573logger.info(574"Requirement %s: %s (%s)", skip_reason, req, req.satisfied_by.version575)576with indent_log():577if self.require_hashes:578logger.debug(579"Since it is already installed, we are trusting this "580"package without checking its hash. To ensure a "581"completely repeatable environment, install into an "582"empty virtualenv."583)584return InstalledDistribution(req).get_metadata_distribution()585586587