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

Mutually exclusive --target, --user and --prefix #4557

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/4111.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pip install ``--target``, ``--user``, and ``--prefix`` are now mutually exclusive.
11 changes: 11 additions & 0 deletions src/pip/_internal/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,11 @@ def run(self, options, args):
"Can not combine '--user' and '--prefix' as they imply "
"different installation locations"
)
if options.target_dir:
raise CommandError(
"Can not combine '--user' and '--target' as they imply "
"different installation locations"
)
if virtualenv_no_global():
raise InstallationError(
"Can not perform a '--user' install. User site-packages "
Expand All @@ -208,6 +213,11 @@ def run(self, options, args):

target_temp_dir = TempDirectory(kind="target")
if options.target_dir:
if options.prefix_path:
raise CommandError(
"Can not combine '--target' and '--prefix' as they imply "
"different installation locations"
)
options.ignore_installed = True
options.target_dir = os.path.abspath(options.target_dir)
if (os.path.exists(options.target_dir) and not
Expand All @@ -220,6 +230,7 @@ def run(self, options, args):
# Create a target directory for using with the target option
target_temp_dir.create()
install_options.append('--home=' + target_temp_dir.path)
install_options.append('--prefix=')

global_options = options.global_options or []

Expand Down
6 changes: 4 additions & 2 deletions src/pip/_internal/locations.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,11 @@ def distutils_scheme(dist_name, user=False, home=None, root=None,
# or user base for installations during finalize_options()
# ideally, we'd prefer a scheme class that has no side-effects.
assert not (user and prefix), "user={0} prefix={1}".format(user, prefix)
i.user = user or i.user
if user:
assert not (home and prefix), "home={0} prefix={1}".format(home, prefix)
assert not (home and user), "home={0} user={1}".format(home, user)
if user or home:
i.prefix = ""
i.user = user or i.user
i.prefix = prefix or i.prefix
i.home = home or i.home
i.root = root or i.root
Expand Down
31 changes: 31 additions & 0 deletions tests/functional/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,37 @@ def test_install_package_conflict_prefix_and_user(script, data):
)


def test_install_package_conflict_target_and_user(script, data):
"""
Test installing a package using pip install --target --user errors out
"""
target_path = script.scratch_path / 'target'
result = script.pip(
'install', '-f', data.find_links, '--no-index', '--user',
'--target', target_path, 'simple==1.0',
expect_error=True, quiet=True,
)
assert (
"Can not combine '--user' and '--target'" in result.stderr
)


def test_install_package_conflict_prefix_and_target(script, data):
"""
Test installing a package using pip install --prefix --targetx errors out
"""
prefix_path = script.scratch_path / 'prefix'
target_path = script.scratch_path / 'target'
result = script.pip(
'install', '-f', data.find_links, '--no-index', '--target',
target_path, '--prefix', prefix_path, 'simple==1.0',
expect_error=True, quiet=True,
)
assert (
"Can not combine '--target' and '--prefix'" in result.stderr
)


# skip on win/py3 for now, see issue #782
@pytest.mark.skipif("sys.platform == 'win32' and sys.version_info >= (3,)")
def test_install_package_that_emits_unicode(script, data):
Expand Down
60 changes: 54 additions & 6 deletions tests/functional/test_install_reqs.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
from tests.lib.local_repos import local_checkout


if os.name == 'posix':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a toxic destructive test - if ever any user with a preconfigured configfile runs it, they will face a lost configuration, and potentially hours or days of figuring why something they knew worked is suddenly broken

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just copied that from the test below to avoid code duplication in many places. I have no idea how this config file is supposed to be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code in current master does the same.

if os.name == 'posix':
user_filename = ".pydistutils.cfg"
else:
user_filename = "pydistutils.cfg"
user_cfg = os.path.join(os.path.expanduser('~'), user_filename)

So yes, it points to user HOME, but I thought that test framework should mock that, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RonnyPfannschmidt so any ideas how to change that?

Copy link
Member

@pradyunsg pradyunsg Sep 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine since we have an isolate fixture to isolate the tests from the actual system, which is used when virtualenv fixture is used.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK, by the time we isolate, it's too late, since this is done during collection.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right. The execution during collection can be deferred using a function then, with the appropriate comment on why it's a function.

distutils_cfg = os.path.expanduser('~/.pydistutils.cfg')
else:
distutils_cfg = os.path.expanduser('~\\.pydistutils.cfg')

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't necessary. os.path.expanduser('~') is fine without needing to be factored out into a function. Also see below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/ works on both POSIX and Windows, so no need for this conditional. On the other hand, you've changed the Windows filename to now include an initial . - was that deliberate or is this a bug? If it is a bug, the tests didn't pick it up so can you look at the tests to confirm why?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing was probably a copy-paste error. Why it doesn't fail is another question.

From https://github.com/python/cpython/blob/6f0eb93183519024cb360162bdd81b9faec97ba6/Lib/distutils/dist.py#L348 configs on Linux and Windows files are different, so 4 tests together that use this setting including https://github.com/techtonik/pip/blob/b395355b0188e64d0224a744b632333a5e3144d7/tests/functional/test_install_reqs.py#L232 are probably indifferent to those settings.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't look into this, because I ditched Windows from my life.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the second thought at least it should be possible to make them fail on both platforms by renaming the file to some nonsense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to write a test that fails on Appveyor, then modify the code to resolve the test failure. Agreed, it's more painful if you don't have a local Windows system, but it's still doable.

On the other hand, this all seems unrelated to the original point of the PR, which is simply about making the options mutually exclusive. Can't the original requirement be satisfied without changing this area of code at all? Sorry if I missed something, but there's no obvious comment in the thread explaining why we need to modify this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't run the single test due to #4878. tox doesn't allow this too, right?

@pytest.mark.network
def test_requirements_file(script):
"""
Expand Down Expand Up @@ -229,13 +235,8 @@ def test_wheel_user_with_prefix_in_pydistutils_cfg(
# Make sure wheel is available in the virtualenv
script.pip('install', 'wheel', '--no-index', '-f', common_wheels)
virtualenv.system_site_packages = True
if os.name == 'posix':
user_filename = ".pydistutils.cfg"
else:
user_filename = "pydistutils.cfg"
user_cfg = os.path.join(os.path.expanduser('~'), user_filename)
script.scratch_path.join("bin").mkdir()
with open(user_cfg, "w") as cfg:
with open(distutils_cfg, "w") as cfg:
cfg.write(textwrap.dedent("""
[install]
prefix=%s""" % script.scratch_path))
Expand All @@ -249,6 +250,53 @@ def test_wheel_user_with_prefix_in_pydistutils_cfg(
assert 'installed requiresupper' in result.stdout


def test_nowheel_user_with_prefix_in_pydistutils_cfg(script, data, virtualenv):
virtualenv.system_site_packages = True
with open(distutils_cfg, "w") as cfg:
cfg.write(textwrap.dedent("""
[install]
prefix=%s""" % script.scratch_path))

result = script.pip('install', '--no-binary=:all:', '--user', '--no-index',
'-f', data.find_links, 'requiresupper',
expect_stderr=True)
assert 'installed requiresupper' in result.stdout


@pytest.mark.network
def test_wheel_target_with_prefix_in_pydistutils_cfg(
script, data, virtualenv, common_wheels):
# pip needs `wheel` to build `requiresupper` wheel before installing
script.pip('install', 'wheel', '--no-index', '-f', common_wheels)
with open(distutils_cfg, "w") as cfg:
cfg.write(textwrap.dedent("""
[install]
prefix=%s""" % script.scratch_path))

target_path = script.scratch_path / 'target'
result = script.pip('install', '--target', target_path, '--no-index',
'-f', data.find_links, '-f', common_wheels,
'requiresupper')
# Check that we are really installing a wheel
assert 'Running setup.py install for requiresupper' not in result.stdout
assert 'installed requiresupper' in result.stdout


def test_nowheel_target_with_prefix_in_pydistutils_cfg(script, data,
virtualenv):
with open(distutils_cfg, "w") as cfg:
cfg.write(textwrap.dedent("""
[install]
prefix=%s""" % script.scratch_path))

target_path = script.scratch_path / 'target'
result = script.pip('install', '--no-binary=:all:',
'--target', target_path,
'--no-index', '-f', data.find_links, 'requiresupper',
expect_stderr=True)
assert 'installed requiresupper' in result.stdout


def test_install_option_in_requirements_file(script, data, virtualenv):
"""
Test --install-option in requirements file overrides same option in cli
Expand Down