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

New resolver: implement --user #7997

Merged
merged 2 commits into from
May 15, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
36 changes: 34 additions & 2 deletions src/pip/_internal/resolution/resolvelib/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@
InstallationError,
UnsupportedPythonVersion,
)
from pip._internal.utils.misc import get_installed_distributions
from pip._internal.utils.misc import (
dist_in_site_packages,
dist_in_usersite,
get_installed_distributions,
)
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
from pip._internal.utils.virtualenv import running_under_virtualenv

from .candidates import (
AlreadyInstalledCandidate,
Expand Down Expand Up @@ -49,6 +54,7 @@ def __init__(
finder, # type: PackageFinder
preparer, # type: RequirementPreparer
make_install_req, # type: InstallRequirementProvider
use_user_site, # type: bool
force_reinstall, # type: bool
ignore_installed, # type: bool
ignore_requires_python, # type: bool
Expand All @@ -62,6 +68,7 @@ def __init__(
self.preparer = preparer
self._python_candidate = RequiresPythonCandidate(py_version_info)
self._make_install_req_from_spec = make_install_req
self._use_user_site = use_user_site
self._force_reinstall = force_reinstall
self._ignore_requires_python = ignore_requires_python
self._upgrade_strategy = upgrade_strategy
Expand Down Expand Up @@ -199,7 +206,32 @@ def make_requires_python_requirement(self, specifier):
def should_reinstall(self, candidate):
# type: (Candidate) -> bool
# TODO: Are there more cases this needs to return True? Editable?
return candidate.name in self._installed_dists
dist = self._installed_dists.get(candidate.name)
if dist is None: # Not installed, no uninstallation required.
return False

# We're installing into global site. The current installation must
# be uninstalled, no matter it's in global or user site, because the
# user site installation has precedence over global.
if not self._use_user_site:
return True

# We're installing into user site. Remove the user site installation.
if dist_in_usersite(dist):
return True

# We're installing into user site, but the installed incompatible
# package is in global site. We can't uninstall that, and would let
# the new user installation to "shadow" it. But shadowing won't work
# in virtual environments, so we error out.
if running_under_virtualenv() and dist_in_site_packages(dist):
raise InstallationError(
"Will not install to the user site because it will "
"lack sys.path precedence to {} in {}".format(
dist.project_name, dist.location,
)
)
return False

def _report_requires_python_error(
self,
Expand Down
1 change: 1 addition & 0 deletions src/pip/_internal/resolution/resolvelib/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def __init__(
finder=finder,
preparer=preparer,
make_install_req=make_install_req,
use_user_site=use_user_site,
force_reinstall=force_reinstall,
ignore_installed=ignore_installed,
ignore_requires_python=ignore_requires_python,
Expand Down
223 changes: 223 additions & 0 deletions tests/functional/test_new_resolver_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import os
import textwrap

import pytest

from tests.lib import create_basic_wheel_for_package


@pytest.mark.incompatible_with_test_venv
def test_new_resolver_install_user(script):
create_basic_wheel_for_package(script, "base", "0.1.0")
result = script.pip(
"install", "--unstable-feature=resolver",
"--no-cache-dir", "--no-index",
"--find-links", script.scratch_path,
"--user",
"base",
)
assert script.user_site / "base" in result.files_created, str(result)


@pytest.mark.incompatible_with_test_venv
def test_new_resolver_install_user_satisfied_by_global_site(script):
"""
An install a matching version to user site should re-use a global site
installation if it satisfies.
"""
create_basic_wheel_for_package(script, "base", "1.0.0")

script.pip(
"install", "--unstable-feature=resolver",
"--no-cache-dir", "--no-index",
"--find-links", script.scratch_path,
"base==1.0.0",
)
result = script.pip(
"install", "--unstable-feature=resolver",
"--no-cache-dir", "--no-index",
"--find-links", script.scratch_path,
"--user",
"base==1.0.0",
)

assert script.user_site / "base" not in result.files_created, str(result)


@pytest.mark.incompatible_with_test_venv
def test_new_resolver_install_user_conflict_in_user_site(script):
"""
Installing a different version in user site should uninstall an existing
different version in user site.
"""
create_basic_wheel_for_package(script, "base", "1.0.0")
create_basic_wheel_for_package(script, "base", "2.0.0")

script.pip(
"install", "--unstable-feature=resolver",
"--no-cache-dir", "--no-index",
"--find-links", script.scratch_path,
"--user",
"base==2.0.0",
)

result = script.pip(
"install", "--unstable-feature=resolver",
"--no-cache-dir", "--no-index",
"--find-links", script.scratch_path,
"--user",
"base==1.0.0",
)

base_1_dist_info = script.user_site / "base-1.0.0.dist-info"
base_2_dist_info = script.user_site / "base-2.0.0.dist-info"

assert base_1_dist_info in result.files_created, str(result)
assert base_2_dist_info not in result.files_created, str(result)


@pytest.mark.incompatible_with_test_venv
def test_new_resolver_install_user_in_virtualenv_with_conflict_fails(script):
create_basic_wheel_for_package(script, "base", "1.0.0")
create_basic_wheel_for_package(script, "base", "2.0.0")

script.pip(
"install", "--unstable-feature=resolver",
"--no-cache-dir", "--no-index",
"--find-links", script.scratch_path,
"base==2.0.0",
)
result = script.pip(
"install", "--unstable-feature=resolver",
"--no-cache-dir", "--no-index",
"--find-links", script.scratch_path,
"--user",
"base==1.0.0",
expect_error=True,
)

error_message = (
"Will not install to the user site because it will lack sys.path "
"precedence to base in {}"
).format(os.path.normcase(script.site_packages_path))
assert error_message in result.stderr


@pytest.fixture()
def patch_dist_in_site_packages(virtualenv):
# Since the tests are run from a virtualenv, and to avoid the "Will not
# install to the usersite because it will lack sys.path precedence..."
# error: Monkey patch `dist_in_site_packages` in the resolver module so
# it's possible to install a conflicting distribution in the user site.
virtualenv.sitecustomize = textwrap.dedent("""
def dist_in_site_packages(dist):
return False

from pip._internal.resolution.resolvelib import factory
factory.dist_in_site_packages = dist_in_site_packages
""")


@pytest.mark.incompatible_with_test_venv
@pytest.mark.usefixtures("patch_dist_in_site_packages")
def test_new_resolver_install_user_reinstall_global_site(script):
"""
Specifying --force-reinstall makes a different version in user site,
ignoring the matching installation in global site.
"""
create_basic_wheel_for_package(script, "base", "1.0.0")

script.pip(
"install", "--unstable-feature=resolver",
"--no-cache-dir", "--no-index",
"--find-links", script.scratch_path,
"base==1.0.0",
)
result = script.pip(
"install", "--unstable-feature=resolver",
"--no-cache-dir", "--no-index",
"--find-links", script.scratch_path,
"--user",
"--force-reinstall",
"base==1.0.0",
)

assert script.user_site / "base" in result.files_created, str(result)

site_packages_content = set(os.listdir(script.site_packages_path))
assert "base" in site_packages_content


@pytest.mark.incompatible_with_test_venv
@pytest.mark.usefixtures("patch_dist_in_site_packages")
def test_new_resolver_install_user_conflict_in_global_site(script):
"""
Installing a different version in user site should ignore an existing
different version in global site, and simply add to the user site.
"""
create_basic_wheel_for_package(script, "base", "1.0.0")
create_basic_wheel_for_package(script, "base", "2.0.0")

script.pip(
"install", "--unstable-feature=resolver",
"--no-cache-dir", "--no-index",
"--find-links", script.scratch_path,
"base==1.0.0",
)

result = script.pip(
"install", "--unstable-feature=resolver",
"--no-cache-dir", "--no-index",
"--find-links", script.scratch_path,
"--user",
"base==2.0.0",
)

base_2_dist_info = script.user_site / "base-2.0.0.dist-info"
assert base_2_dist_info in result.files_created, str(result)

site_packages_content = set(os.listdir(script.site_packages_path))
assert "base-1.0.0.dist-info" in site_packages_content


@pytest.mark.incompatible_with_test_venv
@pytest.mark.usefixtures("patch_dist_in_site_packages")
def test_new_resolver_install_user_conflict_in_global_and_user_sites(script):
"""
Installing a different version in user site should ignore an existing
different version in global site, but still upgrade the user site.
"""
create_basic_wheel_for_package(script, "base", "1.0.0")
create_basic_wheel_for_package(script, "base", "2.0.0")

script.pip(
"install", "--unstable-feature=resolver",
"--no-cache-dir", "--no-index",
"--find-links", script.scratch_path,
"base==2.0.0",
)
script.pip(
"install", "--unstable-feature=resolver",
"--no-cache-dir", "--no-index",
"--find-links", script.scratch_path,
"--user",
"--force-reinstall",
"base==2.0.0",
)

result = script.pip(
"install", "--unstable-feature=resolver",
"--no-cache-dir", "--no-index",
"--find-links", script.scratch_path,
"--user",
"base==1.0.0",
)

base_1_dist_info = script.user_site / "base-1.0.0.dist-info"
base_2_dist_info = script.user_site / "base-2.0.0.dist-info"

assert base_1_dist_info in result.files_created, str(result)
assert base_2_dist_info in result.files_deleted, str(result)

site_packages_content = set(os.listdir(script.site_packages_path))
assert "base-2.0.0.dist-info" in site_packages_content
1 change: 1 addition & 0 deletions tests/unit/resolution_resolvelib/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def factory(finder, preparer):
finder=finder,
preparer=preparer,
make_install_req=install_req_from_line,
use_user_site=False,
force_reinstall=False,
ignore_installed=False,
ignore_requires_python=False,
Expand Down