@ -1,6 +1,3 @@
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
import functools
import logging
import os
@ -8,8 +5,8 @@ import shutil
import sys
import uuid
import zipfile
from enum import Enum
from optparse import Values
from pathlib import Path
from typing import Any , Collection , Dict , Iterable , List , Optional , Sequence , Union
from pip . _vendor . packaging . markers import Marker
@ -21,7 +18,7 @@ from pip._vendor.packaging.version import parse as parse_version
from pip . _vendor . pyproject_hooks import BuildBackendHookCaller
from pip . _internal . build_env import BuildEnvironment , NoOpBuildEnvironment
from pip . _internal . exceptions import InstallationError , LegacyInstallFailure
from pip . _internal . exceptions import InstallationError , PreviousBuildDirError
from pip . _internal . locations import get_scheme
from pip . _internal . metadata import (
BaseDistribution ,
@ -40,15 +37,10 @@ from pip._internal.operations.build.metadata_legacy import (
from pip . _internal . operations . install . editable_legacy import (
install_editable as install_editable_legacy ,
)
from pip . _internal . operations . install . legacy import install as install_legacy
from pip . _internal . operations . install . wheel import install_wheel
from pip . _internal . pyproject import load_pyproject_toml , make_pyproject_path
from pip . _internal . req . req_uninstall import UninstallPathSet
from pip . _internal . utils . deprecation import LegacyInstallReason , deprecated
from pip . _internal . utils . direct_url_helpers import (
direct_url_for_editable ,
direct_url_from_link ,
)
from pip . _internal . utils . deprecation import deprecated
from pip . _internal . utils . hashes import Hashes
from pip . _internal . utils . misc import (
ConfiguredBuildBackendHookCaller ,
@ -56,11 +48,14 @@ from pip._internal.utils.misc import (
backup_dir ,
display_path ,
hide_url ,
is_installable_dir ,
redact_auth_from_requirement ,
redact_auth_from_url ,
)
from pip . _internal . utils . packaging import safe_extra
from pip . _internal . utils . subprocess import runner_with_spinner_message
from pip . _internal . utils . temp_dir import TempDirectory , tempdir_kinds
from pip . _internal . utils . unpacking import unpack_file
from pip . _internal . utils . virtualenv import running_under_virtualenv
from pip . _internal . vcs import vcs
@ -83,10 +78,10 @@ class InstallRequirement:
markers : Optional [ Marker ] = None ,
use_pep517 : Optional [ bool ] = None ,
isolated : bool = False ,
install_options : Optional [ List [ str ] ] = None ,
* ,
global_options : Optional [ List [ str ] ] = None ,
hash_options : Optional [ Dict [ str , List [ str ] ] ] = None ,
config_settings : Optional [ Dict [ str , str ] ] = None ,
config_settings : Optional [ Dict [ str , Union [ str , List [ str ] ] ] ] = None ,
constraint : bool = False ,
extras : Collection [ str ] = ( ) ,
user_supplied : bool = False ,
@ -98,7 +93,6 @@ class InstallRequirement:
self . constraint = constraint
self . editable = editable
self . permit_editable_wheels = permit_editable_wheels
self . legacy_install_reason : Optional [ LegacyInstallReason ] = None
# source_dir is the local directory where the linked requirement is
# located, or unpacked. In case unpacking is needed, creating and
@ -111,11 +105,17 @@ class InstallRequirement:
if link . is_file :
self . source_dir = os . path . normpath ( os . path . abspath ( link . file_path ) )
# original_link is the direct URL that was provided by the user for the
# requirement, either directly or via a constraints file.
if link is None and req and req . url :
# PEP 508 URL requirement
link = Link ( req . url )
self . link = self . original_link = link
self . original_link_is_in_wheel_cache = False
# When this InstallRequirement is a wheel obtained from the cache of locally
# built wheels, this is the source link corresponding to the cache entry, which
# was used to download and build the cached wheel.
self . cached_wheel_source_link : Optional [ Link ] = None
# Information about the location of the artifact that was downloaded . This
# property is guaranteed to be set in resolver results.
@ -129,7 +129,7 @@ class InstallRequirement:
if extras :
self . extras = extras
elif req :
self . extras = { safe_extra ( extra ) for extra in req . extras }
self . extras = req . extras
else :
self . extras = set ( )
if markers is None and req :
@ -146,7 +146,6 @@ class InstallRequirement:
# Set to True after successful installation
self . install_succeeded : Optional [ bool ] = None
# Supplied options
self . install_options = install_options if install_options else [ ]
self . global_options = global_options if global_options else [ ]
self . hash_options = hash_options if hash_options else { }
self . config_settings = config_settings
@ -185,9 +184,12 @@ class InstallRequirement:
# This requirement needs more preparation before it can be built
self . needs_more_preparation = False
# This requirement needs to be unpacked before it can be installed.
self . _archive_source : Optional [ Path ] = None
def __str__ ( self ) - > str :
if self . req :
s = str ( self . req )
s = redact_auth_from_requirement ( self . req )
if self . link :
s + = " from {} " . format ( redact_auth_from_url ( self . link . url ) )
elif self . link :
@ -246,15 +248,22 @@ class InstallRequirement:
@property
def specifier ( self ) - > SpecifierSet :
assert self . req is not None
return self . req . specifier
@property
def is_direct ( self ) - > bool :
""" Whether this requirement was specified as a direct URL. """
return self . original_link is not None
@property
def is_pinned ( self ) - > bool :
""" Return whether I am pinned to an exact version.
For example , some - package == 1.2 is pinned ; some - package > 1.2 is not .
"""
specifiers = self . specifier
assert self . req is not None
specifiers = self . req . specifier
return len ( specifiers ) == 1 and next ( iter ( specifiers ) ) . operator in { " == " , " === " }
def match_markers ( self , extras_requested : Optional [ Iterable [ str ] ] = None ) - > bool :
@ -264,7 +273,12 @@ class InstallRequirement:
extras_requested = ( " " , )
if self . markers is not None :
return any (
self . markers . evaluate ( { " extra " : extra } ) for extra in extras_requested
self . markers . evaluate ( { " extra " : extra } )
# TODO: Remove these two variants when packaging is upgraded to
# support the marker comparison logic specified in PEP 685.
or self . markers . evaluate ( { " extra " : safe_extra ( extra ) } )
or self . markers . evaluate ( { " extra " : canonicalize_name ( extra ) } )
for extra in extras_requested
)
else :
return True
@ -295,8 +309,14 @@ class InstallRequirement:
"""
good_hashes = self . hash_options . copy ( )
link = self . link if trust_internet else self . original_link
if trust_internet :
link = self . link
elif self . is_direct and self . user_supplied :
link = self . original_link
else :
link = None
if link and link . hash :
assert link . hash_name is not None
good_hashes . setdefault ( link . hash_name , [ ] ) . append ( link . hash )
return Hashes ( good_hashes )
@ -306,6 +326,7 @@ class InstallRequirement:
return None
s = str ( self . req )
if self . comes_from :
comes_from : Optional [ str ]
if isinstance ( self . comes_from , str ) :
comes_from = self . comes_from
else :
@ -337,7 +358,7 @@ class InstallRequirement:
# When parallel builds are enabled, add a UUID to the build directory
# name so multiple builds do not interfere with each other.
dir_name : str = canonicalize_name ( self . name)
dir_name : str = canonicalize_name ( self . req. name)
if parallel_builds :
dir_name = f " { dir_name } _ { uuid . uuid4 ( ) . hex } "
@ -380,6 +401,7 @@ class InstallRequirement:
)
def warn_on_mismatching_name ( self ) - > None :
assert self . req is not None
metadata_name = canonicalize_name ( self . metadata [ " Name " ] )
if canonicalize_name ( self . req . name ) == metadata_name :
# Everything is fine.
@ -440,9 +462,16 @@ class InstallRequirement:
return False
return self . link . is_wheel
@property
def is_wheel_from_cache ( self ) - > bool :
# When True, it means that this InstallRequirement is a local wheel file in the
# cache of locally built wheels.
return self . cached_wheel_source_link is not None
# Things valid for sdists
@property
def unpacked_source_directory ( self ) - > str :
assert self . source_dir , f " No source dir for { self } "
return os . path . join (
self . source_dir , self . link and self . link . subdirectory_fragment or " "
)
@ -479,6 +508,15 @@ class InstallRequirement:
)
if pyproject_toml_data is None :
if self . config_settings :
deprecated (
reason = f " Config settings are ignored for project { self } . " ,
replacement = (
" to use --use-pep517 or add a "
" pyproject.toml file to the project "
) ,
gone_in = " 24.0 " ,
)
self . use_pep517 = False
return
@ -520,7 +558,7 @@ class InstallRequirement:
Under PEP 517 and PEP 660 , call the backend hook to prepare the metadata .
Under legacy processing , call setup . py egg - info .
"""
assert self . source_dir
assert self . source_dir , f " No source dir for { self } "
details = self . name or f " from { self . link } "
if self . use_pep517 :
@ -569,8 +607,10 @@ class InstallRequirement:
if self . metadata_directory :
return get_directory_distribution ( self . metadata_directory )
elif self . local_file_path and self . is_wheel :
assert self . req is not None
return get_wheel_distribution (
FilesystemWheel ( self . local_file_path ) , canonicalize_name ( self . name )
FilesystemWheel ( self . local_file_path ) ,
canonicalize_name ( self . req . name ) ,
)
raise AssertionError (
f " InstallRequirement { self } has no metadata directory and no wheel: "
@ -578,9 +618,9 @@ class InstallRequirement:
)
def assert_source_matches_version ( self ) - > None :
assert self . source_dir
assert self . source_dir , f " No source dir for { self } "
version = self . metadata [ " version " ]
if self . req . specifier and version not in self . req . specifier :
if self . req and self . req . specifier and version not in self . req . specifier :
logger . warning (
" Requested %s , but installing version %s " ,
self ,
@ -617,6 +657,27 @@ class InstallRequirement:
parallel_builds = parallel_builds ,
)
def needs_unpacked_archive ( self , archive_source : Path ) - > None :
assert self . _archive_source is None
self . _archive_source = archive_source
def ensure_pristine_source_checkout ( self ) - > None :
""" Ensure the source directory has not yet been built in. """
assert self . source_dir is not None
if self . _archive_source is not None :
unpack_file ( str ( self . _archive_source ) , self . source_dir )
elif is_installable_dir ( self . source_dir ) :
# If a checkout exists, it's unwise to keep going.
# version inconsistencies are logged later, but do not fail
# the installation.
raise PreviousBuildDirError (
f " pip can ' t proceed with requirements ' { self } ' due to a "
f " pre-existing build directory ( { self . source_dir } ). This is likely "
" due to a previous installation that failed . pip is "
" being responsible and not assuming it can delete this. "
" Please delete it and try again. "
)
# For editable installations
def update_editable ( self ) - > None :
if not self . link :
@ -673,9 +734,10 @@ class InstallRequirement:
name = name . replace ( os . path . sep , " / " )
return name
assert self . req is not None
path = os . path . join ( parentdir , path )
name = _clean_zip_name ( path , rootdir )
return self . name + " / " + name
return self . req. name + " / " + name
def archive ( self , build_dir : Optional [ str ] ) - > None :
""" Saves archive to provided build_dir.
@ -746,7 +808,6 @@ class InstallRequirement:
def install (
self ,
install_options : List [ str ] ,
global_options : Optional [ Sequence [ str ] ] = None ,
root : Optional [ str ] = None ,
home : Optional [ str ] = None ,
@ -755,8 +816,9 @@ class InstallRequirement:
use_user_site : bool = False ,
pycompile : bool = True ,
) - > None :
assert self . req is not None
scheme = get_scheme (
self . name,
self . req. name,
user = use_user_site ,
home = home ,
root = root ,
@ -764,15 +826,13 @@ class InstallRequirement:
prefix = prefix ,
)
global_options = global_options if global_options is not None else [ ]
if self . editable and not self . is_wheel :
install_editable_legacy (
install_options ,
global_options ,
global_options = global_options if global_options is not None else [ ] ,
prefix = prefix ,
home = home ,
use_user_site = use_user_site ,
name = self . name,
name = self . req. name,
setup_py_path = self . setup_py_path ,
isolated = self . isolated ,
build_env = self . build_env ,
@ -781,82 +841,23 @@ class InstallRequirement:
self . install_succeeded = True
return
if self . is_wheel :
assert self . local_file_path
direct_url = None
# TODO this can be refactored to direct_url = self.download_info
if self . editable :
direct_url = direct_url_for_editable ( self . unpacked_source_directory )
elif self . original_link :
direct_url = direct_url_from_link (
self . original_link ,
self . source_dir ,
self . original_link_is_in_wheel_cache ,
)
install_wheel (
self . name ,
self . local_file_path ,
scheme = scheme ,
req_description = str ( self . req ) ,
pycompile = pycompile ,
warn_script_location = warn_script_location ,
direct_url = direct_url ,
requested = self . user_supplied ,
)
self . install_succeeded = True
return
# TODO: Why don't we do this for editable installs?
# Extend the list of global and install options passed on to
# the setup.py call with the ones from the requirements file.
# Options specified in requirements file override those
# specified on the command line, since the last option given
# to setup.py is the one that is used.
global_options = list ( global_options ) + self . global_options
install_options = list ( install_options ) + self . install_options
try :
if (
self . legacy_install_reason is not None
and self . legacy_install_reason . emit_before_install
) :
self . legacy_install_reason . emit_deprecation ( self . name )
success = install_legacy (
install_options = install_options ,
global_options = global_options ,
root = root ,
home = home ,
prefix = prefix ,
use_user_site = use_user_site ,
pycompile = pycompile ,
scheme = scheme ,
setup_py_path = self . setup_py_path ,
isolated = self . isolated ,
req_name = self . name ,
build_env = self . build_env ,
unpacked_source_directory = self . unpacked_source_directory ,
req_description = str ( self . req ) ,
)
except LegacyInstallFailure as exc :
self . install_succeeded = False
raise exc
except Exception :
self . install_succeeded = True
raise
self . install_succeeded = success
if (
success
and self . legacy_install_reason is not None
and self . legacy_install_reason . emit_after_success
) :
self . legacy_install_reason . emit_deprecation ( self . name )
assert self . is_wheel
assert self . local_file_path
install_wheel (
self . req . name ,
self . local_file_path ,
scheme = scheme ,
req_description = str ( self . req ) ,
pycompile = pycompile ,
warn_script_location = warn_script_location ,
direct_url = self . download_info if self . is_direct else None ,
requested = self . user_supplied ,
)
self . install_succeeded = True
def check_invalid_constraint_type ( req : InstallRequirement ) - > str :
# Check for unsupported forms
problem = " "
if not req . name :
@ -893,54 +894,21 @@ def _has_option(options: Values, reqs: List[InstallRequirement], option: str) ->
return False
def _install_option_ignored (
install_options : List [ str ] , reqs : List [ InstallRequirement ]
) - > bool :
for req in reqs :
if ( install_options or req . install_options ) and not req . use_pep517 :
return False
return True
class LegacySetupPyOptionsCheckMode ( Enum ) :
INSTALL = 1
WHEEL = 2
DOWNLOAD = 3
def check_legacy_setup_py_options (
options : Values ,
reqs : List [ InstallRequirement ] ,
mode : LegacySetupPyOptionsCheckMode ,
) - > None :
has_install_options = _has_option ( options , reqs , " install_options " )
has_build_options = _has_option ( options , reqs , " build_options " )
has_global_options = _has_option ( options , reqs , " global_options " )
legacy_setup_py_options_present = (
has_install_options or has_build_options or has_global_options
)
if not legacy_setup_py_options_present :
return
options . format_control . disallow_binaries ( )
logger . warning (
" Implying --no-binary=:all: due to the presence of "
" --build-option / --global-option / --install-option. "
" Consider using --config-settings for more flexibility. " ,
)
if mode == LegacySetupPyOptionsCheckMode . INSTALL and has_install_options :
if _install_option_ignored ( options . install_options , reqs ) :
logger . warning (
" Ignoring --install-option when building using PEP 517 " ,
)
else :
deprecated (
reason = (
" --install-option is deprecated because "
" it forces pip to use the ' setup.py install ' "
" command which is itself deprecated. "
) ,
issue = 11358 ,
replacement = " to use --config-settings " ,
gone_in = " 23.1 " ,
)
if has_build_options or has_global_options :
deprecated (
reason = " --build-option and --global-option are deprecated. " ,
issue = 11859 ,
replacement = " to use --config-settings " ,
gone_in = " 24.0 " ,
)
logger . warning (
" Implying --no-binary=:all: due to the presence of "
" --build-option / --global-option. "
)
options . format_control . disallow_binaries ( )