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,