Path: blob/master/venv/Lib/site-packages/pip/_internal/operations/prepare.py
811 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 shutil1011from pip._vendor import requests12from pip._vendor.six import PY21314from pip._internal.distributions import (15make_distribution_for_install_requirement,16)17from pip._internal.distributions.installed import InstalledDistribution18from pip._internal.exceptions import (19DirectoryUrlHashUnsupported,20HashMismatch,21HashUnpinned,22InstallationError,23PreviousBuildDirError,24VcsHashUnsupported,25)26from pip._internal.utils.filesystem import copy2_fixed27from pip._internal.utils.hashes import MissingHashes28from pip._internal.utils.logging import indent_log29from pip._internal.utils.misc import (30display_path,31hide_url,32path_to_display,33rmtree,34)35from pip._internal.utils.temp_dir import TempDirectory36from pip._internal.utils.typing import MYPY_CHECK_RUNNING37from pip._internal.utils.unpacking import unpack_file38from pip._internal.vcs import vcs3940if MYPY_CHECK_RUNNING:41from typing import (42Callable, List, Optional, Tuple,43)4445from mypy_extensions import TypedDict4647from pip._internal.distributions import AbstractDistribution48from pip._internal.index.package_finder import PackageFinder49from pip._internal.models.link import Link50from pip._internal.network.download import Downloader51from pip._internal.req.req_install import InstallRequirement52from pip._internal.req.req_tracker import RequirementTracker53from pip._internal.utils.hashes import Hashes5455if PY2:56CopytreeKwargs = TypedDict(57'CopytreeKwargs',58{59'ignore': Callable[[str, List[str]], List[str]],60'symlinks': bool,61},62total=False,63)64else:65CopytreeKwargs = TypedDict(66'CopytreeKwargs',67{68'copy_function': Callable[[str, str], None],69'ignore': Callable[[str, List[str]], List[str]],70'ignore_dangling_symlinks': bool,71'symlinks': bool,72},73total=False,74)7576logger = logging.getLogger(__name__)777879def _get_prepared_distribution(80req, # type: InstallRequirement81req_tracker, # type: RequirementTracker82finder, # type: PackageFinder83build_isolation # type: bool84):85# type: (...) -> AbstractDistribution86"""Prepare a distribution for installation.87"""88abstract_dist = make_distribution_for_install_requirement(req)89with req_tracker.track(req):90abstract_dist.prepare_distribution_metadata(finder, build_isolation)91return abstract_dist929394def unpack_vcs_link(link, location):95# type: (Link, str) -> None96vcs_backend = vcs.get_backend_for_scheme(link.scheme)97assert vcs_backend is not None98vcs_backend.unpack(location, url=hide_url(link.url))99100101class File(object):102def __init__(self, path, content_type):103# type: (str, str) -> None104self.path = path105self.content_type = content_type106107108def get_http_url(109link, # type: Link110downloader, # type: Downloader111download_dir=None, # type: Optional[str]112hashes=None, # type: Optional[Hashes]113):114# type: (...) -> File115temp_dir = TempDirectory(kind="unpack", globally_managed=True)116# If a download dir is specified, is the file already downloaded there?117already_downloaded_path = None118if download_dir:119already_downloaded_path = _check_download_dir(120link, download_dir, hashes121)122123if already_downloaded_path:124from_path = already_downloaded_path125content_type = mimetypes.guess_type(from_path)[0]126else:127# let's download to a tmp dir128from_path, content_type = _download_http_url(129link, downloader, temp_dir.path, hashes130)131132return File(from_path, content_type)133134135def _copy2_ignoring_special_files(src, dest):136# type: (str, str) -> None137"""Copying special files is not supported, but as a convenience to users138we skip errors copying them. This supports tools that may create e.g.139socket files in the project source directory.140"""141try:142copy2_fixed(src, dest)143except shutil.SpecialFileError as e:144# SpecialFileError may be raised due to either the source or145# destination. If the destination was the cause then we would actually146# care, but since the destination directory is deleted prior to147# copy we ignore all of them assuming it is caused by the source.148logger.warning(149"Ignoring special file error '%s' encountered copying %s to %s.",150str(e),151path_to_display(src),152path_to_display(dest),153)154155156def _copy_source_tree(source, target):157# type: (str, str) -> None158target_abspath = os.path.abspath(target)159target_basename = os.path.basename(target_abspath)160target_dirname = os.path.dirname(target_abspath)161162def ignore(d, names):163# type: (str, List[str]) -> List[str]164skipped = [] # type: List[str]165if d == source:166# Pulling in those directories can potentially be very slow,167# exclude the following directories if they appear in the top168# level dir (and only it).169# See discussion at https://github.com/pypa/pip/pull/6770170skipped += ['.tox', '.nox']171if os.path.abspath(d) == target_dirname:172# Prevent an infinite recursion if the target is in source.173# This can happen when TMPDIR is set to ${PWD}/...174# and we copy PWD to TMPDIR.175skipped += [target_basename]176return skipped177178kwargs = dict(ignore=ignore, symlinks=True) # type: CopytreeKwargs179180if not PY2:181# Python 2 does not support copy_function, so we only ignore182# errors on special file copy in Python 3.183kwargs['copy_function'] = _copy2_ignoring_special_files184185shutil.copytree(source, target, **kwargs)186187188def get_file_url(189link, # type: Link190download_dir=None, # type: Optional[str]191hashes=None # type: Optional[Hashes]192):193# type: (...) -> File194"""Get file and optionally check its hash.195"""196# If a download dir is specified, is the file already there and valid?197already_downloaded_path = None198if download_dir:199already_downloaded_path = _check_download_dir(200link, download_dir, hashes201)202203if already_downloaded_path:204from_path = already_downloaded_path205else:206from_path = link.file_path207208# If --require-hashes is off, `hashes` is either empty, the209# link's embedded hash, or MissingHashes; it is required to210# match. If --require-hashes is on, we are satisfied by any211# hash in `hashes` matching: a URL-based or an option-based212# one; no internet-sourced hash will be in `hashes`.213if hashes:214hashes.check_against_path(from_path)215216content_type = mimetypes.guess_type(from_path)[0]217218return File(from_path, content_type)219220221def unpack_url(222link, # type: Link223location, # type: str224downloader, # type: Downloader225download_dir=None, # type: Optional[str]226hashes=None, # type: Optional[Hashes]227):228# type: (...) -> Optional[File]229"""Unpack link into location, downloading if required.230231:param hashes: A Hashes object, one of whose embedded hashes must match,232or HashMismatch will be raised. If the Hashes is empty, no matches are233required, and unhashable types of requirements (like VCS ones, which234would ordinarily raise HashUnsupported) are allowed.235"""236# non-editable vcs urls237if link.is_vcs:238unpack_vcs_link(link, location)239return None240241# If it's a url to a local directory242if link.is_existing_dir():243if os.path.isdir(location):244rmtree(location)245_copy_source_tree(link.file_path, location)246return None247248# file urls249if link.is_file:250file = get_file_url(link, download_dir, hashes=hashes)251252# http urls253else:254file = get_http_url(255link,256downloader,257download_dir,258hashes=hashes,259)260261# unpack the archive to the build dir location. even when only downloading262# archives, they have to be unpacked to parse dependencies263unpack_file(file.path, location, file.content_type)264265return file266267268def _download_http_url(269link, # type: Link270downloader, # type: Downloader271temp_dir, # type: str272hashes, # type: Optional[Hashes]273):274# type: (...) -> Tuple[str, str]275"""Download link url into temp_dir using provided session"""276download = downloader(link)277278file_path = os.path.join(temp_dir, download.filename)279with open(file_path, 'wb') as content_file:280for chunk in download.chunks:281content_file.write(chunk)282283if hashes:284hashes.check_against_path(file_path)285286return file_path, download.response.headers.get('content-type', '')287288289def _check_download_dir(link, download_dir, hashes):290# type: (Link, str, Optional[Hashes]) -> Optional[str]291""" Check download_dir for previously downloaded file with correct hash292If a correct file is found return its path else None293"""294download_path = os.path.join(download_dir, link.filename)295296if not os.path.exists(download_path):297return None298299# If already downloaded, does its hash match?300logger.info('File was already downloaded %s', download_path)301if hashes:302try:303hashes.check_against_path(download_path)304except HashMismatch:305logger.warning(306'Previously-downloaded file %s has bad hash. '307'Re-downloading.',308download_path309)310os.unlink(download_path)311return None312return download_path313314315class RequirementPreparer(object):316"""Prepares a Requirement317"""318319def __init__(320self,321build_dir, # type: str322download_dir, # type: Optional[str]323src_dir, # type: str324wheel_download_dir, # type: Optional[str]325build_isolation, # type: bool326req_tracker, # type: RequirementTracker327downloader, # type: Downloader328finder, # type: PackageFinder329require_hashes, # type: bool330use_user_site, # type: bool331):332# type: (...) -> None333super(RequirementPreparer, self).__init__()334335self.src_dir = src_dir336self.build_dir = build_dir337self.req_tracker = req_tracker338self.downloader = downloader339self.finder = finder340341# Where still-packed archives should be written to. If None, they are342# not saved, and are deleted immediately after unpacking.343self.download_dir = download_dir344345# Where still-packed .whl files should be written to. If None, they are346# written to the download_dir parameter. Separate to download_dir to347# permit only keeping wheel archives for pip wheel.348self.wheel_download_dir = wheel_download_dir349350# NOTE351# download_dir and wheel_download_dir overlap semantically and may352# be combined if we're willing to have non-wheel archives present in353# the wheelhouse output by 'pip wheel'.354355# Is build isolation allowed?356self.build_isolation = build_isolation357358# Should hash-checking be required?359self.require_hashes = require_hashes360361# Should install in user site-packages?362self.use_user_site = use_user_site363364@property365def _download_should_save(self):366# type: () -> bool367if not self.download_dir:368return False369370if os.path.exists(self.download_dir):371return True372373logger.critical('Could not find download directory')374raise InstallationError(375"Could not find or access download directory '{}'"376.format(self.download_dir))377378def prepare_linked_requirement(379self,380req, # type: InstallRequirement381):382# type: (...) -> AbstractDistribution383"""Prepare a requirement that would be obtained from req.link384"""385assert req.link386link = req.link387388# TODO: Breakup into smaller functions389if link.scheme == 'file':390path = link.file_path391logger.info('Processing %s', display_path(path))392else:393logger.info('Collecting %s', req.req or req)394395download_dir = self.download_dir396if link.is_wheel and self.wheel_download_dir:397# when doing 'pip wheel` we download wheels to a398# dedicated dir.399download_dir = self.wheel_download_dir400401if link.is_wheel:402if download_dir:403# When downloading, we only unpack wheels to get404# metadata.405autodelete_unpacked = True406else:407# When installing a wheel, we use the unpacked408# wheel.409autodelete_unpacked = False410else:411# We always delete unpacked sdists after pip runs.412autodelete_unpacked = True413414with indent_log():415# Since source_dir is only set for editable requirements.416assert req.source_dir is None417req.ensure_has_source_dir(self.build_dir, autodelete_unpacked)418# If a checkout exists, it's unwise to keep going. version419# inconsistencies are logged later, but do not fail the420# installation.421# FIXME: this won't upgrade when there's an existing422# package unpacked in `req.source_dir`423if os.path.exists(os.path.join(req.source_dir, 'setup.py')):424raise PreviousBuildDirError(425"pip can't proceed with requirements '{}' due to a"426" pre-existing build directory ({}). This is "427"likely due to a previous installation that failed"428". pip is being responsible and not assuming it "429"can delete this. Please delete it and try again."430.format(req, req.source_dir)431)432433# Now that we have the real link, we can tell what kind of434# requirements we have and raise some more informative errors435# than otherwise. (For example, we can raise VcsHashUnsupported436# for a VCS URL rather than HashMissing.)437if self.require_hashes:438# We could check these first 2 conditions inside439# unpack_url and save repetition of conditions, but then440# we would report less-useful error messages for441# unhashable requirements, complaining that there's no442# hash provided.443if link.is_vcs:444raise VcsHashUnsupported()445elif link.is_existing_dir():446raise DirectoryUrlHashUnsupported()447if not req.original_link and not req.is_pinned:448# Unpinned packages are asking for trouble when a new449# version is uploaded. This isn't a security check, but450# it saves users a surprising hash mismatch in the451# future.452#453# file:/// URLs aren't pinnable, so don't complain454# about them not being pinned.455raise HashUnpinned()456457hashes = req.hashes(trust_internet=not self.require_hashes)458if self.require_hashes and not hashes:459# Known-good hashes are missing for this requirement, so460# shim it with a facade object that will provoke hash461# computation and then raise a HashMissing exception462# showing the user what the hash should be.463hashes = MissingHashes()464465try:466local_file = unpack_url(467link, req.source_dir, self.downloader, download_dir,468hashes=hashes,469)470except requests.HTTPError as exc:471logger.critical(472'Could not install requirement %s because of error %s',473req,474exc,475)476raise InstallationError(477'Could not install requirement {} because of HTTP '478'error {} for URL {}'.format(req, exc, link)479)480481# For use in later processing, preserve the file path on the482# requirement.483if local_file:484req.local_file_path = local_file.path485486abstract_dist = _get_prepared_distribution(487req, self.req_tracker, self.finder, self.build_isolation,488)489490if download_dir:491if link.is_existing_dir():492logger.info('Link is a directory, ignoring download_dir')493elif local_file:494download_location = os.path.join(495download_dir, link.filename496)497if not os.path.exists(download_location):498shutil.copy(local_file.path, download_location)499logger.info(500'Saved %s', display_path(download_location)501)502503if self._download_should_save:504# Make a .zip of the source_dir we already created.505if link.is_vcs:506req.archive(self.download_dir)507return abstract_dist508509def prepare_editable_requirement(510self,511req, # type: InstallRequirement512):513# type: (...) -> AbstractDistribution514"""Prepare an editable requirement515"""516assert req.editable, "cannot prepare a non-editable req as editable"517518logger.info('Obtaining %s', req)519520with indent_log():521if self.require_hashes:522raise InstallationError(523'The editable requirement {} cannot be installed when '524'requiring hashes, because there is no single file to '525'hash.'.format(req)526)527req.ensure_has_source_dir(self.src_dir)528req.update_editable(not self._download_should_save)529530abstract_dist = _get_prepared_distribution(531req, self.req_tracker, self.finder, self.build_isolation,532)533534if self._download_should_save:535req.archive(self.download_dir)536req.check_if_exists(self.use_user_site)537538return abstract_dist539540def prepare_installed_requirement(541self,542req, # type: InstallRequirement543skip_reason # type: str544):545# type: (...) -> AbstractDistribution546"""Prepare an already-installed requirement547"""548assert req.satisfied_by, "req should have been satisfied but isn't"549assert skip_reason is not None, (550"did not get skip reason skipped but req.satisfied_by "551"is set to {}".format(req.satisfied_by)552)553logger.info(554'Requirement %s: %s (%s)',555skip_reason, req, req.satisfied_by.version556)557with indent_log():558if self.require_hashes:559logger.debug(560'Since it is already installed, we are trusting this '561'package without checking its hash. To ensure a '562'completely repeatable environment, install into an '563'empty virtualenv.'564)565abstract_dist = InstalledDistribution(req)566567return abstract_dist568569570