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

[WIP] Implement working scheme #4871

Closed
wants to merge 4 commits into from
Closed
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
2 changes: 1 addition & 1 deletion src/pip/_internal/commands/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ def run(self, options, args):
finder=finder,
session=session,
wheel_cache=None,
use_user_site=False,
working_scheme="global",
upgrade_strategy="to-satisfy-only",
force_reinstall=False,
ignore_dependencies=options.ignore_dependencies,
Expand Down
110 changes: 94 additions & 16 deletions src/pip/_internal/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,25 @@
import operator
import os
import shutil
import site

from pip._internal import cmdoptions
from pip._internal.basecommand import RequirementCommand
from pip._internal.cache import WheelCache
from pip._internal.exceptions import (
CommandError, InstallationError, PreviousBuildDirError
)
from pip._internal.locations import distutils_scheme, virtualenv_no_global
from pip._internal.locations import (
distutils_scheme, running_under_virtualenv, virtualenv_no_global
)
from pip._internal.operations.prepare import RequirementPreparer
from pip._internal.req import RequirementSet
from pip._internal.resolve import Resolver
from pip._internal.status_codes import ERROR
from pip._internal.utils.filesystem import check_path_owner
from pip._internal.utils.misc import ensure_dir, get_installed_version
from pip._internal.utils.misc import (
ensure_dir, get_installed_version, guess_correct_working_scheme
)
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.wheel import WheelBuilder

Expand Down Expand Up @@ -75,14 +80,37 @@ def __init__(self, *args, **kw):
'<dir>. Use --upgrade to replace existing packages in <dir> '
'with new versions.'
)

cmd_opts.add_option(
'--scheme',
dest='working_scheme',
default=None,
help=(
"Determines where to install the package. Valid values are "
"global, user and venv."
)
)

cmd_opts.add_option(
'--user',
dest='use_user_site',
action='store_true',
help="Install to the Python user install directory for your "
"platform. Typically ~/.local/, or %APPDATA%\\Python on "
"Windows. (See the Python documentation for site.USER_BASE "
"for full details.)")
help=(
"Shorthand for --scheme user; if --scheme is passed, "
"this option is ignored."
),
)

cmd_opts.add_option(
'--global',
dest='use_global_site',
action='store_true',
help=(
"Shorthand for --scheme global; if --scheme is passed, "
"this option is ignored."
),
)

cmd_opts.add_option(
'--root',
dest='root_path',
Expand Down Expand Up @@ -192,17 +220,67 @@ def run(self, options, args):

options.src_dir = os.path.abspath(options.src_dir)
install_options = options.install_options or []
if options.use_user_site:

# When not passed a scheme, try to figure out what scheme to be used.
# Also handles --user and --global.
if options.working_scheme is None:
if options.use_user_site and options.use_global_site:
raise CommandError(
"Unable to determine which scheme to use for "
"installation (got both --global and --user). "
"Please specify scheme explicitly using --scheme."
)
elif options.use_user_site or options.use_global_site:
if options.use_user_site:
options.working_scheme = "user"
else:
assert options.use_global_site
options.working_scheme = "global"
# If we're in an virtualenv, we want to use the venv scheme.
elif running_under_virtualenv():
options.working_scheme = "venv"
# user scheme doesn't make sense if user site-packages is disabled
# or if a prefix is to be used.
elif not site.ENABLE_USER_SITE or options.prefix_path:
options.working_scheme = "global"
else:
# NOTE: We do backwards compatibility checks here. Eventually,
# this line should just be:
# options.working_scheme = "user"
options.working_scheme = guess_correct_working_scheme()

# Ensure no one can ever depend on these variables.
del options.use_global_site
del options.use_user_site

# Ensure the given scheme is a valid one.
if options.working_scheme not in ["user", "global", "venv"]:
raise CommandError(
"Got unknown scheme {!r}. "
"Should be one of 'global', 'user' or 'venv'."
.format(options.working_scheme)
)

# Sanity checks for working schemes
if options.working_scheme == "venv":
if not running_under_virtualenv():
raise CommandError(
"Can not use 'venv' scheme when a virtualenv is not "
"active."
)
options.require_venv = True
elif options.working_scheme == "user":
if options.prefix_path:
raise CommandError(
"Can not combine '--user' and '--prefix' as they imply "
"different installation locations"
"Can not combine '--scheme user' and '--prefix' as they "
"imply different installation locations."
)
if virtualenv_no_global():
raise InstallationError(
"Can not perform a '--user' install. User site-packages "
"are not visible in this virtualenv."
"Can not perform a user scheme install. User "
"site-packages are not visible in this virtualenv."
)

install_options.append('--user')
install_options.append('--prefix=')

Expand Down Expand Up @@ -246,7 +324,7 @@ def run(self, options, args):
target_dir=target_temp_dir.path,
pycompile=options.compile,
require_hashes=options.require_hashes,
use_user_site=options.use_user_site,
working_scheme=options.working_scheme,
)

self.populate_requirement_set(
Expand All @@ -266,7 +344,7 @@ def run(self, options, args):
finder=finder,
session=session,
wheel_cache=wheel_cache,
use_user_site=options.use_user_site,
working_scheme=options.working_scheme,
upgrade_strategy=upgrade_strategy,
force_reinstall=options.force_reinstall,
ignore_dependencies=options.ignore_dependencies,
Expand Down Expand Up @@ -301,7 +379,7 @@ def run(self, options, args):
)

possible_lib_locations = get_lib_location_guesses(
user=options.use_user_site,
user=options.working_scheme == "user",
home=target_temp_dir.path,
root=options.root_path,
prefix=options.prefix_path,
Expand All @@ -326,11 +404,11 @@ def run(self, options, args):
except EnvironmentError as e:
message_parts = []

user_option_part = "Consider using the `--user` option"
user_option_part = "Consider using `--scheme user` option"
permissions_part = "Check the permissions"

if e.errno == errno.EPERM:
if not options.use_user_site:
if options.working_scheme != "user":
message_parts.extend([
user_option_part, " or ",
permissions_part.lower(),
Expand Down
2 changes: 1 addition & 1 deletion src/pip/_internal/commands/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def run(self, options, args):
finder=finder,
session=session,
wheel_cache=wheel_cache,
use_user_site=False,
working_scheme="global",
upgrade_strategy="to-satisfy-only",
force_reinstall=False,
ignore_dependencies=options.ignore_dependencies,
Expand Down
8 changes: 4 additions & 4 deletions src/pip/_internal/req/req_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def __init__(self, req, comes_from, source_dir=None, editable=False,
self.install_succeeded = None
# UninstallPathSet of uninstalled distribution (for possible rollback)
self.uninstalled_pathset = None
self.use_user_site = False
self.working_scheme = "global"
self.target_dir = None
self.options = options if options else {}
self.pycompile = pycompile
Expand Down Expand Up @@ -840,7 +840,7 @@ def get_install_args(self, global_options, record_filename, root, prefix):
else:
install_args += ["--no-compile"]

if running_under_virtualenv():
if self.working_scheme == "venv":
py_ver_str = 'python' + sysconfig.get_python_version()
install_args += ['--install-headers',
os.path.join(sys.prefix, 'include', 'site',
Expand Down Expand Up @@ -913,7 +913,7 @@ def check_if_exists(self):
existing_dist = pkg_resources.get_distribution(
self.req.name
)
if self.use_user_site:
if self.working_scheme == "user":
if dist_in_usersite(existing_dist):
self.conflicts_with = existing_dist
elif (running_under_virtualenv() and
Expand All @@ -935,7 +935,7 @@ def move_wheel_files(self, wheeldir, root=None, prefix=None,
warn_script_location=True):
move_wheel_files(
self.name, self.req, wheeldir,
user=self.use_user_site,
user=self.working_scheme == "user",
home=self.target_dir,
root=root,
prefix=prefix,
Expand Down
8 changes: 4 additions & 4 deletions src/pip/_internal/req/req_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
class RequirementSet(object):

def __init__(self,
require_hashes=False, target_dir=None, use_user_site=False,
pycompile=True):
require_hashes=False, target_dir=None,
working_scheme="global", pycompile=True):
"""Create a RequirementSet.

:param wheel_cache: The pip wheel cache, for passing to
Expand All @@ -29,7 +29,7 @@ def __init__(self,
self.unnamed_requirements = []
self.successfully_downloaded = []
self.reqs_to_cleanup = []
self.use_user_site = use_user_site
self.working_scheme = working_scheme
self.target_dir = target_dir # set from --target option
self.pycompile = pycompile
# Maps from install_req -> dependencies_of_install_req
Expand Down Expand Up @@ -81,7 +81,7 @@ def add_requirement(self, install_req, parent_req_name=None,
wheel.filename
)

install_req.use_user_site = self.use_user_site
install_req.working_scheme = self.working_scheme
install_req.target_dir = self.target_dir
install_req.pycompile = self.pycompile
install_req.is_direct = (parent_req_name is None)
Expand Down
6 changes: 3 additions & 3 deletions src/pip/_internal/resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class Resolver(object):

_allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"}

def __init__(self, preparer, session, finder, wheel_cache, use_user_site,
def __init__(self, preparer, session, finder, wheel_cache, working_scheme,
ignore_dependencies, ignore_installed, ignore_requires_python,
force_reinstall, isolated, upgrade_strategy):
super(Resolver, self).__init__()
Expand All @@ -54,7 +54,7 @@ def __init__(self, preparer, session, finder, wheel_cache, use_user_site,
self.ignore_dependencies = ignore_dependencies
self.ignore_installed = ignore_installed
self.ignore_requires_python = ignore_requires_python
self.use_user_site = use_user_site
self.working_scheme = working_scheme

def resolve(self, requirement_set):
"""Resolve what operations need to be done
Expand Down Expand Up @@ -120,7 +120,7 @@ def _set_req_to_reinstall(self, req):
"""
# Don't uninstall the conflict if doing a user install and the
# conflict is not a user install.
if not self.use_user_site or dist_in_usersite(req.satisfied_by):
if self.working_scheme != "user" or dist_in_usersite(req.satisfied_by):
req.conflicts_with = req.satisfied_by
req.satisfied_by = None

Expand Down
40 changes: 38 additions & 2 deletions src/pip/_internal/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import subprocess
import sys
import tarfile
import tempfile
import zipfile
from collections import deque

Expand All @@ -28,8 +29,8 @@
from pip._internal.compat import console_to_str, expanduser, stdlib_pkgs
from pip._internal.exceptions import InstallationError
from pip._internal.locations import (
running_under_virtualenv, site_packages, user_site, virtualenv_no_global,
write_delete_marker_file
distutils_scheme, running_under_virtualenv, site_packages, user_site,
virtualenv_no_global, write_delete_marker_file
)

if PY2:
Expand Down Expand Up @@ -877,3 +878,38 @@ def enum(*sequential, **named):
reverse = dict((value, key) for key, value in enums.items())
enums['reverse_mapping'] = reverse
return type('Enum', (), enums)


def guess_correct_working_scheme():
# XXX: This is purely for addressing backwards compatibility concerns.

def _can_create_file_in(folder_path):
"""Returns whether a file can be created in the given folder
"""
# If the given folder does not exist, try to create it. If there's
# going to be a successful installation, the folder probably needs to
# be created anyway.
try:
ensure_dir(folder_path)
except EnvironmentError:
return False

# Try to create a temporary file in the folder and delete it.
try:
with tempfile.TemporaryFile(dir=folder_path):
pass
except EnvironmentError:
return False

return True

# This is the "smart" portion. Check if there's any potential global folder
# for which pip doesn't have the required permissions. If there's any such
# location, pip should perform a user installation instead.
for folder in distutils_scheme("").values():
if not _can_create_file_in(folder):
return "user"

# Assume the user wants to do a global installation. This is the most
# backwards compatible policy; preserving behaviour for `sudo pip install`
return "global"
2 changes: 1 addition & 1 deletion tests/functional/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,7 @@ def test_install_package_conflict_prefix_and_user(script, data):
expect_error=True, quiet=True,
)
assert (
"Can not combine '--user' and '--prefix'" in result.stderr
"Can not combine '--scheme user' and '--prefix'" in result.stderr
)


Expand Down
6 changes: 3 additions & 3 deletions tests/functional/test_install_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def test_install_subversion_usersite_editable_with_distribute(
tmpdir.join("cache"),
)
)
result.assert_installed('INITools', use_user_site=True)
result.assert_installed('INITools', in_user_site=True)

def test_install_curdir_usersite(self, script, virtualenv, data):
"""
Expand Down Expand Up @@ -98,8 +98,8 @@ def test_install_user_venv_nositepkgs_fails(self, script, data):
expect_error=True,
)
assert (
"Can not perform a '--user' install. User site-packages are not "
"visible in this virtualenv." in result.stderr
"Can not perform a user scheme install. User site-packages are "
"not visible in this virtualenv." in result.stderr
)

@pytest.mark.network
Expand Down
Loading