@ -1,6 +1,3 @@
# The following comment should be removed at some point in the future.
# mypy: strict-optional=False
import functools
import functools
import logging
import logging
import os
import os
@ -8,8 +5,8 @@ import shutil
import sys
import sys
import uuid
import uuid
import zipfile
import zipfile
from enum import Enum
from optparse import Values
from optparse import Values
from pathlib import Path
from typing import Any , Collection , Dict , Iterable , List , Optional , Sequence , Union
from typing import Any , Collection , Dict , Iterable , List , Optional , Sequence , Union
from pip . _vendor . packaging . markers import Marker
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 . _vendor . pyproject_hooks import BuildBackendHookCaller
from pip . _internal . build_env import BuildEnvironment , NoOpBuildEnvironment
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 . locations import get_scheme
from pip . _internal . metadata import (
from pip . _internal . metadata import (
BaseDistribution ,
BaseDistribution ,
@ -40,15 +37,10 @@ from pip._internal.operations.build.metadata_legacy import (
from pip . _internal . operations . install . editable_legacy import (
from pip . _internal . operations . install . editable_legacy import (
install_editable as install_editable_legacy ,
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 . operations . install . wheel import install_wheel
from pip . _internal . pyproject import load_pyproject_toml , make_pyproject_path
from pip . _internal . pyproject import load_pyproject_toml , make_pyproject_path
from pip . _internal . req . req_uninstall import UninstallPathSet
from pip . _internal . req . req_uninstall import UninstallPathSet
from pip . _internal . utils . deprecation import LegacyInstallReason , deprecated
from pip . _internal . utils . deprecation import deprecated
from pip . _internal . utils . direct_url_helpers import (
direct_url_for_editable ,
direct_url_from_link ,
)
from pip . _internal . utils . hashes import Hashes
from pip . _internal . utils . hashes import Hashes
from pip . _internal . utils . misc import (
from pip . _internal . utils . misc import (
ConfiguredBuildBackendHookCaller ,
ConfiguredBuildBackendHookCaller ,
@ -56,11 +48,14 @@ from pip._internal.utils.misc import (
backup_dir ,
backup_dir ,
display_path ,
display_path ,
hide_url ,
hide_url ,
is_installable_dir ,
redact_auth_from_requirement ,
redact_auth_from_url ,
redact_auth_from_url ,
)
)
from pip . _internal . utils . packaging import safe_extra
from pip . _internal . utils . packaging import safe_extra
from pip . _internal . utils . subprocess import runner_with_spinner_message
from pip . _internal . utils . subprocess import runner_with_spinner_message
from pip . _internal . utils . temp_dir import TempDirectory , tempdir_kinds
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 . utils . virtualenv import running_under_virtualenv
from pip . _internal . vcs import vcs
from pip . _internal . vcs import vcs
@ -83,10 +78,10 @@ class InstallRequirement:
markers : Optional [ Marker ] = None ,
markers : Optional [ Marker ] = None ,
use_pep517 : Optional [ bool ] = None ,
use_pep517 : Optional [ bool ] = None ,
isolated : bool = False ,
isolated : bool = False ,
install_options : Optional [ List [ str ] ] = None ,
* ,
global_options : Optional [ List [ str ] ] = None ,
global_options : Optional [ List [ str ] ] = None ,
hash_options : Optional [ Dict [ str , 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 ,
constraint : bool = False ,
extras : Collection [ str ] = ( ) ,
extras : Collection [ str ] = ( ) ,
user_supplied : bool = False ,
user_supplied : bool = False ,
@ -98,7 +93,6 @@ class InstallRequirement:
self . constraint = constraint
self . constraint = constraint
self . editable = editable
self . editable = editable
self . permit_editable_wheels = permit_editable_wheels
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
# source_dir is the local directory where the linked requirement is
# located, or unpacked. In case unpacking is needed, creating and
# located, or unpacked. In case unpacking is needed, creating and
@ -111,11 +105,17 @@ class InstallRequirement:
if link . is_file :
if link . is_file :
self . source_dir = os . path . normpath ( os . path . abspath ( link . file_path ) )
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 :
if link is None and req and req . url :
# PEP 508 URL requirement
# PEP 508 URL requirement
link = Link ( req . url )
link = Link ( req . url )
self . link = self . original_link = link
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
# Information about the location of the artifact that was downloaded . This
# property is guaranteed to be set in resolver results.
# property is guaranteed to be set in resolver results.
@ -129,7 +129,7 @@ class InstallRequirement:
if extras :
if extras :
self . extras = extras
self . extras = extras
elif req :
elif req :
self . extras = { safe_extra ( extra ) for extra in req . extras }
self . extras = req . extras
else :
else :
self . extras = set ( )
self . extras = set ( )
if markers is None and req :
if markers is None and req :
@ -146,7 +146,6 @@ class InstallRequirement:
# Set to True after successful installation
# Set to True after successful installation
self . install_succeeded : Optional [ bool ] = None
self . install_succeeded : Optional [ bool ] = None
# Supplied options
# Supplied options
self . install_options = install_options if install_options else [ ]
self . global_options = global_options if global_options else [ ]
self . global_options = global_options if global_options else [ ]
self . hash_options = hash_options if hash_options else { }
self . hash_options = hash_options if hash_options else { }
self . config_settings = config_settings
self . config_settings = config_settings
@ -185,9 +184,12 @@ class InstallRequirement:
# This requirement needs more preparation before it can be built
# This requirement needs more preparation before it can be built
self . needs_more_preparation = False
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 :
def __str__ ( self ) - > str :
if self . req :
if self . req :
s = str ( self . req )
s = redact_auth_from_requirement ( self . req )
if self . link :
if self . link :
s + = " from {} " . format ( redact_auth_from_url ( self . link . url ) )
s + = " from {} " . format ( redact_auth_from_url ( self . link . url ) )
elif self . link :
elif self . link :
@ -246,15 +248,22 @@ class InstallRequirement:
@property
@property
def specifier ( self ) - > SpecifierSet :
def specifier ( self ) - > SpecifierSet :
assert self . req is not None
return self . req . specifier
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
@property
def is_pinned ( self ) - > bool :
def is_pinned ( self ) - > bool :
""" Return whether I am pinned to an exact version.
""" Return whether I am pinned to an exact version.
For example , some - package == 1.2 is pinned ; some - package > 1.2 is not .
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 { " == " , " === " }
return len ( specifiers ) == 1 and next ( iter ( specifiers ) ) . operator in { " == " , " === " }
def match_markers ( self , extras_requested : Optional [ Iterable [ str ] ] = None ) - > bool :
def match_markers ( self , extras_requested : Optional [ Iterable [ str ] ] = None ) - > bool :
@ -264,7 +273,12 @@ class InstallRequirement:
extras_requested = ( " " , )
extras_requested = ( " " , )
if self . markers is not None :
if self . markers is not None :
return any (
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 :
else :
return True
return True
@ -295,8 +309,14 @@ class InstallRequirement:
"""
"""
good_hashes = self . hash_options . copy ( )
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 :
if link and link . hash :
assert link . hash_name is not None
good_hashes . setdefault ( link . hash_name , [ ] ) . append ( link . hash )
good_hashes . setdefault ( link . hash_name , [ ] ) . append ( link . hash )
return Hashes ( good_hashes )
return Hashes ( good_hashes )
@ -306,6 +326,7 @@ class InstallRequirement:
return None
return None
s = str ( self . req )
s = str ( self . req )
if self . comes_from :
if self . comes_from :
comes_from : Optional [ str ]
if isinstance ( self . comes_from , str ) :
if isinstance ( self . comes_from , str ) :
comes_from = self . comes_from
comes_from = self . comes_from
else :
else :
@ -337,7 +358,7 @@ class InstallRequirement:
# When parallel builds are enabled, add a UUID to the build directory
# When parallel builds are enabled, add a UUID to the build directory
# name so multiple builds do not interfere with each other.
# 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 :
if parallel_builds :
dir_name = f " { dir_name } _ { uuid . uuid4 ( ) . hex } "
dir_name = f " { dir_name } _ { uuid . uuid4 ( ) . hex } "
@ -380,6 +401,7 @@ class InstallRequirement:
)
)
def warn_on_mismatching_name ( self ) - > None :
def warn_on_mismatching_name ( self ) - > None :
assert self . req is not None
metadata_name = canonicalize_name ( self . metadata [ " Name " ] )
metadata_name = canonicalize_name ( self . metadata [ " Name " ] )
if canonicalize_name ( self . req . name ) == metadata_name :
if canonicalize_name ( self . req . name ) == metadata_name :
# Everything is fine.
# Everything is fine.
@ -440,9 +462,16 @@ class InstallRequirement:
return False
return False
return self . link . is_wheel
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
# Things valid for sdists
@property
@property
def unpacked_source_directory ( self ) - > str :
def unpacked_source_directory ( self ) - > str :
assert self . source_dir , f " No source dir for { self } "
return os . path . join (
return os . path . join (
self . source_dir , self . link and self . link . subdirectory_fragment or " "
self . source_dir , self . link and self . link . subdirectory_fragment or " "
)
)
@ -479,6 +508,15 @@ class InstallRequirement:
)
)
if pyproject_toml_data is None :
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
self . use_pep517 = False
return
return
@ -520,7 +558,7 @@ class InstallRequirement:
Under PEP 517 and PEP 660 , call the backend hook to prepare the metadata .
Under PEP 517 and PEP 660 , call the backend hook to prepare the metadata .
Under legacy processing , call setup . py egg - info .
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 } "
details = self . name or f " from { self . link } "
if self . use_pep517 :
if self . use_pep517 :
@ -569,8 +607,10 @@ class InstallRequirement:
if self . metadata_directory :
if self . metadata_directory :
return get_directory_distribution ( self . metadata_directory )
return get_directory_distribution ( self . metadata_directory )
elif self . local_file_path and self . is_wheel :
elif self . local_file_path and self . is_wheel :
assert self . req is not None
return get_wheel_distribution (
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 (
raise AssertionError (
f " InstallRequirement { self } has no metadata directory and no wheel: "
f " InstallRequirement { self } has no metadata directory and no wheel: "
@ -578,9 +618,9 @@ class InstallRequirement:
)
)
def assert_source_matches_version ( self ) - > None :
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 " ]
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 (
logger . warning (
" Requested %s , but installing version %s " ,
" Requested %s , but installing version %s " ,
self ,
self ,
@ -617,6 +657,27 @@ class InstallRequirement:
parallel_builds = parallel_builds ,
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
# For editable installations
def update_editable ( self ) - > None :
def update_editable ( self ) - > None :
if not self . link :
if not self . link :
@ -673,9 +734,10 @@ class InstallRequirement:
name = name . replace ( os . path . sep , " / " )
name = name . replace ( os . path . sep , " / " )
return name
return name
assert self . req is not None
path = os . path . join ( parentdir , path )
path = os . path . join ( parentdir , path )
name = _clean_zip_name ( path , rootdir )
name = _clean_zip_name ( path , rootdir )
return self . name + " / " + name
return self . req. name + " / " + name
def archive ( self , build_dir : Optional [ str ] ) - > None :
def archive ( self , build_dir : Optional [ str ] ) - > None :
""" Saves archive to provided build_dir.
""" Saves archive to provided build_dir.
@ -746,7 +808,6 @@ class InstallRequirement:
def install (
def install (
self ,
self ,
install_options : List [ str ] ,
global_options : Optional [ Sequence [ str ] ] = None ,
global_options : Optional [ Sequence [ str ] ] = None ,
root : Optional [ str ] = None ,
root : Optional [ str ] = None ,
home : Optional [ str ] = None ,
home : Optional [ str ] = None ,
@ -755,8 +816,9 @@ class InstallRequirement:
use_user_site : bool = False ,
use_user_site : bool = False ,
pycompile : bool = True ,
pycompile : bool = True ,
) - > None :
) - > None :
assert self . req is not None
scheme = get_scheme (
scheme = get_scheme (
self . name,
self . req. name,
user = use_user_site ,
user = use_user_site ,
home = home ,
home = home ,
root = root ,
root = root ,
@ -764,15 +826,13 @@ class InstallRequirement:
prefix = prefix ,
prefix = prefix ,
)
)
global_options = global_options if global_options is not None else [ ]
if self . editable and not self . is_wheel :
if self . editable and not self . is_wheel :
install_editable_legacy (
install_editable_legacy (
install_options ,
global_options = global_options if global_options is not None else [ ] ,
global_options ,
prefix = prefix ,
prefix = prefix ,
home = home ,
home = home ,
use_user_site = use_user_site ,
use_user_site = use_user_site ,
name = self . name,
name = self . req. name,
setup_py_path = self . setup_py_path ,
setup_py_path = self . setup_py_path ,
isolated = self . isolated ,
isolated = self . isolated ,
build_env = self . build_env ,
build_env = self . build_env ,
@ -781,82 +841,23 @@ class InstallRequirement:
self . install_succeeded = True
self . install_succeeded = True
return
return
if self . is_wheel :
assert self . is_wheel
assert self . local_file_path
assert self . local_file_path
direct_url = None
# TODO this can be refactored to direct_url = self.download_info
install_wheel (
if self . editable :
self . req . name ,
direct_url = direct_url_for_editable ( self . unpacked_source_directory )
self . local_file_path ,
elif self . original_link :
scheme = scheme ,
direct_url = direct_url_from_link (
req_description = str ( self . req ) ,
self . original_link ,
pycompile = pycompile ,
self . source_dir ,
warn_script_location = warn_script_location ,
self . original_link_is_in_wheel_cache ,
direct_url = self . download_info if self . is_direct else None ,
)
requested = self . user_supplied ,
install_wheel (
)
self . name ,
self . install_succeeded = True
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 )
def check_invalid_constraint_type ( req : InstallRequirement ) - > str :
def check_invalid_constraint_type ( req : InstallRequirement ) - > str :
# Check for unsupported forms
# Check for unsupported forms
problem = " "
problem = " "
if not req . name :
if not req . name :
@ -893,54 +894,21 @@ def _has_option(options: Values, reqs: List[InstallRequirement], option: str) ->
return False
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 (
def check_legacy_setup_py_options (
options : Values ,
options : Values ,
reqs : List [ InstallRequirement ] ,
reqs : List [ InstallRequirement ] ,
mode : LegacySetupPyOptionsCheckMode ,
) - > None :
) - > None :
has_install_options = _has_option ( options , reqs , " install_options " )
has_build_options = _has_option ( options , reqs , " build_options " )
has_build_options = _has_option ( options , reqs , " build_options " )
has_global_options = _has_option ( options , reqs , " global_options " )
has_global_options = _has_option ( options , reqs , " global_options " )
legacy_setup_py_options_present = (
if has_build_options or has_global_options :
has_install_options or has_build_options or has_global_options
deprecated (
)
reason = " --build-option and --global-option are deprecated. " ,
if not legacy_setup_py_options_present :
issue = 11859 ,
return
replacement = " to use --config-settings " ,
gone_in = " 24.0 " ,
options . format_control . disallow_binaries ( )
)
logger . warning (
logger . warning (
" Implying --no-binary=:all: due to the presence of "
" Implying --no-binary=:all: due to the presence of "
" --build-option / --global-option / --install-option. "
" --build-option / --global-option. "
" Consider using --config-settings for more flexibility. " ,
)
)
options . format_control . disallow_binaries ( )
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 " ,
)