diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py
index 1ccfd449338..36b05a2ccdd 100644
--- a/src/pip/_internal/commands/download.py
+++ b/src/pip/_internal/commands/download.py
@@ -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,
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index e144e39ed79..6a6942603ba 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -5,6 +5,7 @@
import operator
import os
import shutil
+import site
from pip._internal import cmdoptions
from pip._internal.basecommand import RequirementCommand
@@ -12,13 +13,17 @@
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
@@ -75,14 +80,37 @@ def __init__(self, *args, **kw):
'
. Use --upgrade to replace existing packages in '
'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',
@@ -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=')
@@ -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(
@@ -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,
@@ -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,
@@ -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(),
diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py
index e03983cd124..82e02b750e9 100644
--- a/src/pip/_internal/commands/wheel.py
+++ b/src/pip/_internal/commands/wheel.py
@@ -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,
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
index 43b85665bd6..4a81dfcce56 100644
--- a/src/pip/_internal/req/req_install.py
+++ b/src/pip/_internal/req/req_install.py
@@ -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
@@ -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',
@@ -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
@@ -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,
diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py
index fb07120e880..ab64992b0a8 100644
--- a/src/pip/_internal/req/req_set.py
+++ b/src/pip/_internal/req/req_set.py
@@ -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
@@ -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
@@ -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)
diff --git a/src/pip/_internal/resolve.py b/src/pip/_internal/resolve.py
index 0c22ee9c92c..b38ac5fdf5e 100644
--- a/src/pip/_internal/resolve.py
+++ b/src/pip/_internal/resolve.py
@@ -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__()
@@ -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
@@ -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
diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py
index d9ecbeb8302..5a5b40623bb 100644
--- a/src/pip/_internal/utils/misc.py
+++ b/src/pip/_internal/utils/misc.py
@@ -15,6 +15,7 @@
import subprocess
import sys
import tarfile
+import tempfile
import zipfile
from collections import deque
@@ -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:
@@ -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"
diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py
index 9d137e8d1de..32c9495836b 100644
--- a/tests/functional/test_install.py
+++ b/tests/functional/test_install.py
@@ -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
)
diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py
index cf0cb513773..c9a3d95a73d 100644
--- a/tests/functional/test_install_user.py
+++ b/tests/functional/test_install_user.py
@@ -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):
"""
@@ -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
diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py
index 99af72dc52c..5538d03894b 100644
--- a/tests/lib/__init__.py
+++ b/tests/lib/__init__.py
@@ -170,7 +170,7 @@ def __str__(self):
def assert_installed(self, pkg_name, editable=True, with_files=[],
without_files=[], without_egg_link=False,
- use_user_site=False, sub_dir=False):
+ in_user_site=False, sub_dir=False):
e = self.test_env
if editable:
@@ -182,7 +182,7 @@ def assert_installed(self, pkg_name, editable=True, with_files=[],
without_egg_link = True
pkg_dir = e.site_packages / pkg_name
- if use_user_site:
+ if in_user_site:
egg_link_path = e.user_site / pkg_name + '.egg-link'
else:
egg_link_path = e.site_packages / pkg_name + '.egg-link'
@@ -217,7 +217,7 @@ def assert_installed(self, pkg_name, editable=True, with_files=[],
repr(egg_link_contents))
))
- if use_user_site:
+ if in_user_site:
pth_file = e.user_site / 'easy-install.pth'
else:
pth_file = e.site_packages / 'easy-install.pth'
diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py
index 1e4646cd54a..550957621d4 100644
--- a/tests/unit/test_req.py
+++ b/tests/unit/test_req.py
@@ -44,7 +44,7 @@ def _basic_resolver(self, finder):
return Resolver(
preparer=preparer, wheel_cache=None,
session=PipSession(), finder=finder,
- use_user_site=False, upgrade_strategy="to-satisfy-only",
+ working_scheme="global", upgrade_strategy="to-satisfy-only",
ignore_dependencies=False, ignore_installed=False,
ignore_requires_python=False, force_reinstall=False,
isolated=False,