diff --git a/news/10110.bugfix.rst b/news/10110.bugfix.rst new file mode 100644 index 00000000000..db77c3de1aa --- /dev/null +++ b/news/10110.bugfix.rst @@ -0,0 +1,2 @@ +Pip install's ``--target`` works more like the other 'install schemas'. This +fixes installing namespaced packages. diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 6cf7571e4a6..d28d1b0144a 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -2,8 +2,8 @@ import json import operator import os -import shutil import site +import sys from optparse import SUPPRESS_HELP, Values from typing import List, Optional @@ -34,7 +34,6 @@ from pip._internal.utils.logging import getLogger from pip._internal.utils.misc import ( check_externally_managed, - ensure_dir, get_pip_version, protect_pip_from_modification_on_windows, write_output, @@ -93,12 +92,7 @@ def add_options(self) -> None: dest="target_dir", metavar="dir", default=None, - help=( - "Install packages into . " - "By default this will not replace existing files/folders in " - ". Use --upgrade to replace existing packages in " - "with new versions." - ), + help=("Install packages into ."), ) cmdoptions.add_target_python_options(self.cmd_opts) @@ -299,10 +293,8 @@ def run(self, options: Values, args: List[str]) -> int: isolated_mode=options.isolated_mode, ) - target_temp_dir: Optional[TempDirectory] = None - target_temp_dir_path: Optional[str] = None + target_dir = None if options.target_dir: - options.ignore_installed = True options.target_dir = os.path.abspath(options.target_dir) if ( # fmt: off @@ -314,13 +306,13 @@ def run(self, options: Values, args: List[str]) -> int: "Target path exists but is not a directory, will not continue." ) - # Create a target directory for using with the target option - target_temp_dir = TempDirectory(kind="target") - target_temp_dir_path = target_temp_dir.path - self.enter_context(target_temp_dir) + target_dir = options.target_dir global_options = options.global_options or [] + if options.target_dir is not None and options.target_dir not in sys.path: + sys.path.append(options.target_dir) + session = self.get_default_session(options) target_python = make_target_python(options) @@ -373,7 +365,6 @@ def run(self, options: Values, args: List[str]) -> int: ) self.trace_basic_info(finder) - requirement_set = resolver.resolve( reqs, check_supported_wheels=not options.target_dir ) @@ -453,19 +444,21 @@ def run(self, options: Values, args: List[str]) -> int: to_install, global_options, root=options.root_path, - home=target_temp_dir_path, + home=target_dir, prefix=options.prefix_path, warn_script_location=warn_script_location, use_user_site=options.use_user_site, pycompile=options.compile, + target=True if target_dir else False, ) lib_locations = get_lib_location_guesses( user=options.use_user_site, - home=target_temp_dir_path, + home=target_dir, root=options.root_path, prefix=options.prefix_path, isolated=options.isolated_mode, + target=True if target_dir else False, ) env = get_environment(lib_locations) @@ -505,68 +498,10 @@ def run(self, options: Values, args: List[str]) -> int: return ERROR - if options.target_dir: - assert target_temp_dir - self._handle_target_dir( - options.target_dir, target_temp_dir, options.upgrade - ) if options.root_user_action == "warn": warn_if_run_as_root() - return SUCCESS - def _handle_target_dir( - self, target_dir: str, target_temp_dir: TempDirectory, upgrade: bool - ) -> None: - ensure_dir(target_dir) - - # Checking both purelib and platlib directories for installed - # packages to be moved to target directory - lib_dir_list = [] - - # Checking both purelib and platlib directories for installed - # packages to be moved to target directory - scheme = get_scheme("", home=target_temp_dir.path) - purelib_dir = scheme.purelib - platlib_dir = scheme.platlib - data_dir = scheme.data - - if os.path.exists(purelib_dir): - lib_dir_list.append(purelib_dir) - if os.path.exists(platlib_dir) and platlib_dir != purelib_dir: - lib_dir_list.append(platlib_dir) - if os.path.exists(data_dir): - lib_dir_list.append(data_dir) - - for lib_dir in lib_dir_list: - for item in os.listdir(lib_dir): - if lib_dir == data_dir: - ddir = os.path.join(data_dir, item) - if any(s.startswith(ddir) for s in lib_dir_list[:-1]): - continue - target_item_dir = os.path.join(target_dir, item) - if os.path.exists(target_item_dir): - if not upgrade: - logger.warning( - "Target directory %s already exists. Specify " - "--upgrade to force replacement.", - target_item_dir, - ) - continue - if os.path.islink(target_item_dir): - logger.warning( - "Target directory %s already exists and is " - "a link. pip will not automatically replace " - "links, please remove if replacement is " - "desired.", - target_item_dir, - ) - continue - if os.path.isdir(target_item_dir): - shutil.rmtree(target_item_dir) - else: - os.remove(target_item_dir) - - shutil.move(os.path.join(lib_dir, item), target_item_dir) + return SUCCESS def _determine_conflicts( self, to_install: List[InstallRequirement] @@ -637,6 +572,7 @@ def get_lib_location_guesses( root: Optional[str] = None, isolated: bool = False, prefix: Optional[str] = None, + target: Optional[bool] = False, ) -> List[str]: scheme = get_scheme( "", @@ -646,6 +582,9 @@ def get_lib_location_guesses( isolated=isolated, prefix=prefix, ) + if target and home is not None: + scheme.purelib = home + scheme.platlib = home return [scheme.purelib, scheme.platlib] diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py index 16de903a44c..990791114b4 100644 --- a/src/pip/_internal/req/__init__.py +++ b/src/pip/_internal/req/__init__.py @@ -43,6 +43,7 @@ def install_given_reqs( warn_script_location: bool, use_user_site: bool, pycompile: bool, + target: bool = False, ) -> List[InstallationResult]: """ Install everything in the given list. @@ -77,6 +78,7 @@ def install_given_reqs( warn_script_location=warn_script_location, use_user_site=use_user_site, pycompile=pycompile, + target=target, ) except Exception: # if install did not succeed, rollback previous uninstall diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 7d527959e81..13697fc18a0 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -818,6 +818,7 @@ def install( warn_script_location: bool = True, use_user_site: bool = False, pycompile: bool = True, + target: bool = False, ) -> None: assert self.req is not None scheme = get_scheme( @@ -828,6 +829,10 @@ def install( isolated=self.isolated, prefix=prefix, ) + if target and home is not None: + scheme.purelib = home + scheme.platlib = home + scheme.data = home if self.editable and not self.is_wheel: if self.config_settings: diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index c013f2eaf72..b4783a1b6bf 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1107,7 +1107,8 @@ def test_install_package_with_target(script: PipTestEnvironment) -> None: result = script.pip_install_local("-t", target_dir, "simple==1.0") result.did_create(Path("scratch") / "target" / "simple") - # Test repeated call without --upgrade, no files should have changed + # When using target directory repeated call without --upgrade, + # no files should have changed result = script.pip_install_local( "-t", target_dir,