Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make --target functionality align with other install schemas #12524

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions news/10110.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Pip install's ``--target`` works more like the other 'install schemas'. This
fixes installing namespaced packages.
93 changes: 16 additions & 77 deletions src/pip/_internal/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -93,12 +92,7 @@ def add_options(self) -> None:
dest="target_dir",
metavar="dir",
default=None,
help=(
"Install packages into <dir>. "
"By default this will not replace existing files/folders in "
"<dir>. Use --upgrade to replace existing packages in <dir> "
"with new versions."
),
help=("Install packages into <dir>."),
)
cmdoptions.add_target_python_options(self.cmd_opts)

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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(
"",
Expand All @@ -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]


Expand Down
2 changes: 2 additions & 0 deletions src/pip/_internal/req/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/pip/_internal/req/req_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion tests/functional/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down