Merge branch 'master' into refactor/reduce-action-at-distance

This commit is contained in:
Pradyun Gedam 2018-03-03 02:08:01 +05:30
commit 255a518157
No known key found for this signature in database
GPG Key ID: DA17C4B29CB32E4B
42 changed files with 600 additions and 276 deletions

View File

@ -171,6 +171,28 @@ You can also refer to :ref:`constraints files <Constraints Files>`, like this::
-c some_constraints.txt
.. _`Using Environment Variables`:
Using Environment Variables
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Since version 10, pip supports the use of environment variables inside the
requirements file. You can now store sensitive data (tokens, keys, etc.) in
environment variables and only specify the variable name for your requirements,
letting pip lookup the value at runtime. This approach aligns with the commonly
used `12-factor configuration pattern <https://12factor.net/config>`_.
You have to use the POSIX format for variable names including brackets around
the uppercase name as shown in this example: ``${API_TOKEN}``. pip will attempt
to find the corresponding environment variable defined on the host system at
runtime.
.. note::
There is no support for other variable expansion syntaxes such as
``$VARIABLE`` and ``%VARIABLE%``.
.. _`Example Requirements File`:
Example Requirements File
@ -432,6 +454,21 @@ Tags or revisions can be installed like so::
[-e] bzr+https://bzr.example.com/MyProject/trunk@2019#egg=MyProject
[-e] bzr+http://bzr.example.com/MyProject/trunk@v1.0#egg=MyProject
Using Environment Variables
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Since version 10, pip also makes it possible to use environment variables which
makes it possible to reference private repositories without having to store
access tokens in the requirements file. For example, a private git repository
allowing Basic Auth for authentication can be refenced like this::
[-e] git+http://${AUTH_USER}:${AUTH_PASSWORD}@git.example.com/MyProject#egg=MyProject
[-e] git+https://${AUTH_USER}:${AUTH_PASSWORD}@git.example.com/MyProject#egg=MyProject
.. note::
Only ``${VARIABLE}`` is supported, other formats like ``$VARIABLE`` or
``%VARIABLE%`` won't work.
Finding Packages
++++++++++++++++

1
news/2732.bugfix Normal file
View File

@ -0,0 +1 @@
Interactive setup.py files will no longer hang indefinitely.

2
news/3728.feature Normal file
View File

@ -0,0 +1,2 @@
pip now supports environment variable expansion in requirement files using
only ``${VARIABLE}`` syntax on all platforms.

3
news/4501.feature Normal file
View File

@ -0,0 +1,3 @@
Installing from a local directory or a VCS URL now builds a wheel to install,
rather than running ``setup.py install``. Wheels from these sources are not
cached.

0
news/4799.trivial Normal file
View File

1
news/4876.bugfix Normal file
View File

@ -0,0 +1 @@
Use log level `info` instead of `warning` when ignoring packages due to environment markers.

1
news/4976.bugfix Normal file
View File

@ -0,0 +1 @@
Abort if reading configuration causes encoding errors.

1
news/4982.bugfix Normal file
View File

@ -0,0 +1 @@
Interactive setup.py files will no longer hang indefinitely.

1
news/4999.feature Normal file
View File

@ -0,0 +1 @@
Run 'setup.py develop' inside pep518 build environment.

View File

@ -1 +1 @@
Upgraded distro to 1.0.4.
Upgraded distro to 1.2.0.

View File

@ -114,14 +114,16 @@ class Command(object):
def main(self, args):
options, args = self.parse_args(args)
verbosity = options.verbose - options.quiet
if verbosity >= 1:
# Set verbosity so that it can be used elsewhere.
self.verbosity = options.verbose - options.quiet
if self.verbosity >= 1:
level = "DEBUG"
elif verbosity == -1:
elif self.verbosity == -1:
level = "WARNING"
elif verbosity == -2:
elif self.verbosity == -2:
level = "ERROR"
elif verbosity <= -3:
elif self.verbosity <= -3:
level = "CRITICAL"
else:
level = "INFO"

View File

@ -9,7 +9,7 @@ from distutils.util import strtobool
from pip._vendor.six import string_types
from pip._internal.configuration import Configuration
from pip._internal.configuration import Configuration, ConfigurationError
from pip._internal.utils.misc import get_terminal_size
logger = logging.getLogger(__name__)
@ -177,9 +177,6 @@ class ConfigOptionParser(CustomOptionParser):
the environ. Does a little special handling for certain types of
options (lists)."""
# Load the configuration
self.config.load()
# Accumulate complex default state.
self.values = optparse.Values(self.defaults)
late_eval = set()
@ -224,6 +221,12 @@ class ConfigOptionParser(CustomOptionParser):
# Old, pre-Optik 1.5 behaviour.
return optparse.Values(self.defaults)
# Load the configuration, or error out in case of an error
try:
self.config.load()
except ConfigurationError as err:
self.exit(2, err.args[0])
defaults = self._update_defaults(self.defaults.copy()) # ours
for option in self._get_all_options():
default = defaults.get(option.dest)

View File

@ -0,0 +1,84 @@
"""Build Environment used for isolation during sdist building
"""
import os
from sysconfig import get_paths
from pip._internal.utils.temp_dir import TempDirectory
class BuildEnvironment(object):
"""Creates and manages an isolated environment to install build deps
"""
def __init__(self, no_clean):
self._temp_dir = TempDirectory(kind="build-env")
self._no_clean = no_clean
@property
def path(self):
return self._temp_dir.path
def __enter__(self):
self._temp_dir.create()
self.save_path = os.environ.get('PATH', None)
self.save_pythonpath = os.environ.get('PYTHONPATH', None)
install_scheme = 'nt' if (os.name == 'nt') else 'posix_prefix'
install_dirs = get_paths(install_scheme, vars={
'base': self.path,
'platbase': self.path,
})
scripts = install_dirs['scripts']
if self.save_path:
os.environ['PATH'] = scripts + os.pathsep + self.save_path
else:
os.environ['PATH'] = scripts + os.pathsep + os.defpath
if install_dirs['purelib'] == install_dirs['platlib']:
lib_dirs = install_dirs['purelib']
else:
lib_dirs = install_dirs['purelib'] + os.pathsep + \
install_dirs['platlib']
if self.save_pythonpath:
os.environ['PYTHONPATH'] = lib_dirs + os.pathsep + \
self.save_pythonpath
else:
os.environ['PYTHONPATH'] = lib_dirs
return self.path
def __exit__(self, exc_type, exc_val, exc_tb):
if not self._no_clean:
self._temp_dir.cleanup()
if self.save_path is None:
os.environ.pop('PATH', None)
else:
os.environ['PATH'] = self.save_path
if self.save_pythonpath is None:
os.environ.pop('PYTHONPATH', None)
else:
os.environ['PYTHONPATH'] = self.save_pythonpath
def cleanup(self):
self._temp_dir.cleanup()
class NoOpBuildEnvironment(BuildEnvironment):
"""A no-op drop-in replacement for BuildEnvironment
"""
def __init__(self, no_clean):
pass
def __enter__(self):
pass
def __exit__(self, exc_type, exc_val, exc_tb):
pass
def cleanup(self):
pass

View File

@ -11,6 +11,7 @@ from pip._vendor.packaging.utils import canonicalize_name
from pip._internal import index
from pip._internal.compat import expanduser
from pip._internal.download import path_to_url
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.wheel import InvalidWheelFilename, Wheel
logger = logging.getLogger(__name__)
@ -102,13 +103,18 @@ class Cache(object):
return index.Link(path_to_url(path))
def cleanup(self):
pass
class WheelCache(Cache):
class SimpleWheelCache(Cache):
"""A cache of wheels for future installs.
"""
def __init__(self, cache_dir, format_control):
super(WheelCache, self).__init__(cache_dir, format_control, {"binary"})
super(SimpleWheelCache, self).__init__(
cache_dir, format_control, {"binary"}
)
def get_path_for_link(self, link):
"""Return a directory to store cached wheels for link
@ -127,8 +133,7 @@ class WheelCache(Cache):
"""
parts = self._get_cache_path_parts(link)
# Inside of the base location for cached wheels, expand our parts and
# join them all together.
# Store wheels within the root cache_dir
return os.path.join(self.cache_dir, "wheels", *parts)
def get(self, link, package_name):
@ -148,3 +153,50 @@ class WheelCache(Cache):
return link
return self._link_for_candidate(link, min(candidates)[1])
class EphemWheelCache(SimpleWheelCache):
"""A SimpleWheelCache that creates it's own temporary cache directory
"""
def __init__(self, format_control):
self._temp_dir = TempDirectory(kind="ephem-wheel-cache")
self._temp_dir.create()
super(EphemWheelCache, self).__init__(
self._temp_dir.path, format_control
)
def cleanup(self):
self._temp_dir.cleanup()
class WheelCache(Cache):
"""Wraps EphemWheelCache and SimpleWheelCache into a single Cache
This Cache allows for gracefully degradation, using the ephem wheel cache
when a certain link is not found in the simple wheel cache first.
"""
def __init__(self, cache_dir, format_control):
super(WheelCache, self).__init__(
cache_dir, format_control, {'binary'}
)
self._wheel_cache = SimpleWheelCache(cache_dir, format_control)
self._ephem_cache = EphemWheelCache(format_control)
def get_path_for_link(self, link):
return self._wheel_cache.get_path_for_link(link)
def get_ephem_path_for_link(self, link):
return self._ephem_cache.get_path_for_link(link)
def get(self, link, package_name):
retval = self._wheel_cache.get(link, package_name)
if retval is link:
retval = self._ephem_cache.get(link, package_name)
return retval
def cleanup(self):
self._wheel_cache.cleanup()
self._ephem_cache.cleanup()

View File

@ -89,5 +89,8 @@ class FreezeCommand(Command):
exclude_editable=options.exclude_editable,
)
for line in freeze(**freeze_kwargs):
sys.stdout.write(line + '\n')
try:
for line in freeze(**freeze_kwargs):
sys.stdout.write(line + '\n')
finally:
wheel_cache.cleanup()

View File

@ -224,10 +224,10 @@ class InstallCommand(RequirementCommand):
global_options = options.global_options or []
with self._build_session(options) as session:
finder = self._build_package_finder(options, session)
build_delete = (not (options.no_clean or options.build_dir))
wheel_cache = WheelCache(options.cache_dir, options.format_control)
if options.cache_dir and not check_path_owner(options.cache_dir):
logger.warning(
"The directory '%s' or its parent directory is not owned "
@ -246,18 +246,19 @@ class InstallCommand(RequirementCommand):
require_hashes=options.require_hashes,
)
self.populate_requirement_set(
requirement_set, args, options, finder, session, self.name,
wheel_cache
)
preparer = RequirementPreparer(
build_dir=directory.path,
src_dir=options.src_dir,
download_dir=None,
wheel_download_dir=None,
progress_bar=options.progress_bar,
)
try:
self.populate_requirement_set(
requirement_set, args, options, finder, session,
self.name, wheel_cache
)
preparer = RequirementPreparer(
build_dir=directory.path,
src_dir=options.src_dir,
download_dir=None,
wheel_download_dir=None,
progress_bar=options.progress_bar,
)
resolver = Resolver(
preparer=preparer,
finder=finder,
@ -339,7 +340,7 @@ class InstallCommand(RequirementCommand):
message_parts.append("\n")
logger.error(
"".join(message_parts), exc_info=(options.verbose > 1)
"".join(message_parts), exc_info=(self.verbosity > 1)
)
return ERROR
except PreviousBuildDirError:
@ -349,6 +350,7 @@ class InstallCommand(RequirementCommand):
# Clean up
if not options.no_clean:
requirement_set.cleanup_files()
wheel_cache.cleanup()
if options.target_dir:
self._handle_target_dir(

View File

@ -65,7 +65,7 @@ class UninstallCommand(Command):
)
for req in reqs_to_uninstall.values():
uninstall_pathset = req.uninstall(
auto_confirm=options.yes, verbose=options.verbose != 0
auto_confirm=options.yes, verbose=self.verbosity > 0,
)
if uninstall_pathset:
uninstall_pathset.commit()

View File

@ -146,35 +146,35 @@ class WheelCommand(RequirementCommand):
require_hashes=options.require_hashes,
)
self.populate_requirement_set(
requirement_set, args, options, finder, session, self.name,
wheel_cache
)
preparer = RequirementPreparer(
build_dir=directory.path,
src_dir=options.src_dir,
download_dir=None,
wheel_download_dir=options.wheel_dir,
progress_bar=options.progress_bar,
)
resolver = Resolver(
preparer=preparer,
finder=finder,
session=session,
wheel_cache=wheel_cache,
use_user_site=False,
upgrade_strategy="to-satisfy-only",
force_reinstall=False,
ignore_dependencies=options.ignore_dependencies,
ignore_requires_python=options.ignore_requires_python,
ignore_installed=True,
isolated=options.isolated_mode,
)
resolver.resolve(requirement_set)
try:
self.populate_requirement_set(
requirement_set, args, options, finder, session,
self.name, wheel_cache
)
preparer = RequirementPreparer(
build_dir=directory.path,
src_dir=options.src_dir,
download_dir=None,
wheel_download_dir=options.wheel_dir,
progress_bar=options.progress_bar,
)
resolver = Resolver(
preparer=preparer,
finder=finder,
session=session,
wheel_cache=wheel_cache,
use_user_site=False,
upgrade_strategy="to-satisfy-only",
force_reinstall=False,
ignore_dependencies=options.ignore_dependencies,
ignore_requires_python=options.ignore_requires_python,
ignore_installed=True,
isolated=options.isolated_mode,
)
resolver.resolve(requirement_set)
# build wheels
wb = WheelBuilder(
finder, preparer, wheel_cache,
@ -195,3 +195,4 @@ class WheelCommand(RequirementCommand):
finally:
if not options.no_clean:
requirement_set.cleanup_files()
wheel_cache.cleanup()

View File

@ -11,6 +11,7 @@ Some terminology:
A single word describing where the configuration key-value pair came from
"""
import locale
import logging
import os
@ -283,8 +284,14 @@ class Configuration(object):
# Doing this is useful when modifying and saving files, where we don't
# need to construct a parser.
if os.path.exists(fname):
parser.read(fname)
try:
parser.read(fname)
except UnicodeDecodeError:
raise ConfigurationError((
"ERROR: "
"Configuration file contains invalid %s characters.\n"
"Please fix your configuration, located at %s\n"
) % (locale.getpreferredencoding(False), fname))
return parser
def _load_environment_vars(self):

View File

@ -1,11 +1,15 @@
"""Prepares a distribution for installation
"""
import itertools
import logging
import os
import sys
from copy import copy
from pip._vendor import pkg_resources, requests
from pip._internal.build_env import NoOpBuildEnvironment
from pip._internal.compat import expanduser
from pip._internal.download import (
is_dir_url, is_file_url, is_vcs_url, unpack_url, url_to_path,
@ -14,9 +18,14 @@ from pip._internal.exceptions import (
DirectoryUrlHashUnsupported, HashUnpinned, InstallationError,
PreviousBuildDirError, VcsHashUnsupported,
)
from pip._internal.index import FormatControl
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.hashes import MissingHashes
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import display_path, normalize_path
from pip._internal.utils.misc import (
call_subprocess, display_path, normalize_path,
)
from pip._internal.utils.ui import open_spinner
from pip._internal.vcs import vcs
logger = logging.getLogger(__name__)
@ -38,6 +47,26 @@ def make_abstract_dist(req):
return IsSDist(req)
def _install_build_reqs(finder, prefix, build_requirements):
# NOTE: What follows is not a very good thing.
# Eventually, this should move into the BuildEnvironment class and
# that should handle all the isolation and sub-process invocation.
finder = copy(finder)
finder.format_control = FormatControl(set(), set([":all:"]))
urls = [
finder.find_requirement(
InstallRequirement.from_line(r), upgrade=False).url
for r in build_requirements
]
args = [
sys.executable, '-m', 'pip', 'install', '--ignore-installed',
'--prefix', prefix,
] + list(urls)
with open_spinner("Installing build dependencies") as spinner:
call_subprocess(args, show_stdout=False, spinner=spinner)
class DistAbstraction(object):
"""Abstracts out the wheel vs non-wheel Resolver.resolve() logic.
@ -64,7 +93,7 @@ class DistAbstraction(object):
"""Return a setuptools Dist object."""
raise NotImplementedError(self.dist)
def prep_for_dist(self):
def prep_for_dist(self, finder):
"""Ensure that we can get a Dist for this requirement."""
raise NotImplementedError(self.dist)
@ -75,7 +104,7 @@ class IsWheel(DistAbstraction):
return list(pkg_resources.find_distributions(
self.req.source_dir))[0]
def prep_for_dist(self):
def prep_for_dist(self, finder):
# FIXME:https://github.com/pypa/pip/issues/1112
pass
@ -91,9 +120,27 @@ class IsSDist(DistAbstraction):
)
return dist
def prep_for_dist(self):
self.req.run_egg_info()
self.req.assert_source_matches_version()
def prep_for_dist(self, finder):
# Before calling "setup.py egg_info", we need to set-up the build
# environment.
build_requirements, isolate = self.req.get_pep_518_info()
if 'setuptools' not in build_requirements:
logger.warning(
"This version of pip does not implement PEP 516, so "
"it cannot build a wheel without setuptools. You may need to "
"upgrade to a newer version of pip.")
if not isolate:
self.req.build_env = NoOpBuildEnvironment(no_clean=False)
with self.req.build_env as prefix:
if isolate:
_install_build_reqs(finder, prefix, build_requirements)
self.req.run_egg_info()
self.req.assert_source_matches_version()
class Installed(DistAbstraction):
@ -101,7 +148,7 @@ class Installed(DistAbstraction):
def dist(self, finder):
return self.req.satisfied_by
def prep_for_dist(self):
def prep_for_dist(self, finder):
pass
@ -259,14 +306,15 @@ class RequirementPreparer(object):
(req, exc, req.link)
)
abstract_dist = make_abstract_dist(req)
abstract_dist.prep_for_dist()
abstract_dist.prep_for_dist(finder)
if self._download_should_save:
# Make a .zip of the source_dir we already created.
if req.link.scheme in vcs.all_schemes:
req.archive(self.download_dir)
return abstract_dist
def prepare_editable_requirement(self, req, require_hashes, use_user_site):
def prepare_editable_requirement(self, req, require_hashes, use_user_site,
finder):
"""Prepare an editable requirement
"""
assert req.editable, "cannot prepare a non-editable req as editable"
@ -284,7 +332,7 @@ class RequirementPreparer(object):
req.update_editable(not self._download_should_save)
abstract_dist = make_abstract_dist(req)
abstract_dist.prep_for_dist()
abstract_dist.prep_for_dist(finder)
if self._download_should_save:
req.archive(self.download_dir)

View File

@ -23,6 +23,12 @@ __all__ = ['parse_requirements']
SCHEME_RE = re.compile(r'^(http|https|file):', re.I)
COMMENT_RE = re.compile(r'(^|\s)+#.*$')
# Matches environment variable-style values in '${MY_VARIABLE_1}' with the
# variable name consisting of only uppercase letters, digits or the '_'
# (underscore). This follows the POSIX standard defined in IEEE Std 1003.1,
# 2013 Edition.
ENV_VAR_RE = re.compile(r'(?P<var>\$\{(?P<name>[A-Z0-9_]+)\})')
SUPPORTED_OPTIONS = [
cmdoptions.constraints,
cmdoptions.editable,
@ -94,6 +100,7 @@ def preprocess(content, options):
lines_enum = join_lines(lines_enum)
lines_enum = ignore_comments(lines_enum)
lines_enum = skip_regex(lines_enum, options)
lines_enum = expand_env_variables(lines_enum)
return lines_enum
@ -302,3 +309,30 @@ def skip_regex(lines_enum, options):
pattern = re.compile(skip_regex)
lines_enum = filterfalse(lambda e: pattern.search(e[1]), lines_enum)
return lines_enum
def expand_env_variables(lines_enum):
"""Replace all environment variables that can be retrieved via `os.getenv`.
The only allowed format for environment variables defined in the
requirement file is `${MY_VARIABLE_1}` to ensure two things:
1. Strings that contain a `$` aren't accidentally (partially) expanded.
2. Ensure consistency across platforms for requirement files.
These points are the result of a discusssion on the `github pull
request #3514 <https://github.com/pypa/pip/pull/3514>`_.
Valid characters in variable names follow the `POSIX standard
<http://pubs.opengroup.org/onlinepubs/9699919799/>`_ and are limited
to uppercase letter, digits and the `_` (underscore).
"""
for line_number, line in lines_enum:
for env_var, var_name in ENV_VAR_RE.findall(line):
value = os.getenv(var_name)
if not value:
continue
line = line.replace(env_var, value)
yield line_number, line

View File

@ -12,7 +12,7 @@ import zipfile
from distutils.util import change_root
from email.parser import FeedParser
from pip._vendor import pkg_resources, six
from pip._vendor import pkg_resources, pytoml, six
from pip._vendor.packaging import specifiers
from pip._vendor.packaging.markers import Marker
from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
@ -22,6 +22,7 @@ from pip._vendor.packaging.version import Version
from pip._vendor.pkg_resources import RequirementParseError, parse_requirements
from pip._internal import wheel
from pip._internal.build_env import BuildEnvironment
from pip._internal.compat import native_str
from pip._internal.download import (
is_archive_file, is_url, path_to_url, url_to_path,
@ -126,6 +127,7 @@ class InstallRequirement(object):
self.is_direct = False
self.isolated = isolated
self.build_env = BuildEnvironment(no_clean=True)
@classmethod
def from_editable(cls, editable_req, comes_from=None, isolated=False,
@ -450,6 +452,22 @@ class InstallRequirement(object):
return pp_toml
def get_pep_518_info(self):
"""Get a list of the packages required to build the project, if any,
and a flag indicating whether pyproject.toml is present, indicating
that the build should be isolated.
Build requirements can be specified in a pyproject.toml, as described
in PEP 518. If this file exists but doesn't specify build
requirements, pip will default to installing setuptools and wheel.
"""
if os.path.isfile(self.pyproject_toml):
with open(self.pyproject_toml) as f:
pp_toml = pytoml.load(f)
build_sys = pp_toml.get('build-system', {})
return (build_sys.get('requires', ['setuptools', 'wheel']), True)
return (['setuptools', 'wheel'], False)
def run_egg_info(self):
assert self.source_dir
if self.name:
@ -866,6 +884,7 @@ class InstallRequirement(object):
rmtree(self.source_dir)
self.source_dir = None
self._temp_build_dir.cleanup()
self.build_env.cleanup()
def install_editable(self, install_options,
global_options=(), prefix=None):
@ -880,19 +899,20 @@ class InstallRequirement(object):
with indent_log():
# FIXME: should we do --install-headers here too?
call_subprocess(
[
sys.executable,
'-c',
SETUPTOOLS_SHIM % self.setup_py
] +
list(global_options) +
['develop', '--no-deps'] +
list(install_options),
with self.build_env:
call_subprocess(
[
sys.executable,
'-c',
SETUPTOOLS_SHIM % self.setup_py
] +
list(global_options) +
['develop', '--no-deps'] +
list(install_options),
cwd=self.setup_py_dir,
show_stdout=False,
)
cwd=self.setup_py_dir,
show_stdout=False,
)
self.install_succeeded = True

View File

@ -54,16 +54,16 @@ class RequirementSet(object):
already be added. Note that None implies that this is a user
supplied requirement, vs an inferred one.
:param extras_requested: an iterable of extras used to evaluate the
environement markers.
environment markers.
:return: Additional requirements to scan. That is either [] if
the requirement is not applicable, or [install_req] if the
requirement is applicable and has just been added.
"""
name = install_req.name
if not install_req.match_markers(extras_requested):
logger.warning("Ignoring %s: markers '%s' don't match your "
"environment", install_req.name,
install_req.markers)
logger.info("Ignoring %s: markers '%s' don't match your "
"environment", install_req.name,
install_req.markers)
return []
# This check has to come after we filter requirements with the

View File

@ -187,7 +187,8 @@ class Resolver(object):
if req.editable:
return self.preparer.prepare_editable_requirement(
req, self.require_hashes, self.use_user_site,
req, self.require_hashes, self.use_user_site, self.finder,
)
# satisfied_by is only evaluated by calling _check_skip_installed,
@ -220,7 +221,9 @@ class Resolver(object):
if req.satisfied_by:
should_modify = (
self.upgrade_strategy != "to-satisfy-only" or
self.force_reinstall or self.ignore_installed
self.force_reinstall or
self.ignore_installed or
req.link.scheme == 'file'
)
if should_modify:
self._set_req_to_reinstall(req)
@ -244,11 +247,12 @@ class Resolver(object):
return []
req_to_install.prepared = True
abstract_dist = self._get_abstract_dist_for(req_to_install)
# register tmp src for cleanup in case something goes wrong
requirement_set.reqs_to_cleanup.append(req_to_install)
abstract_dist = self._get_abstract_dist_for(req_to_install)
# Parse and return dependencies
dist = abstract_dist.dist(self.finder)
try:

View File

@ -678,9 +678,10 @@ def call_subprocess(cmd, show_stdout=True, cwd=None,
env.pop(name, None)
try:
proc = subprocess.Popen(
cmd, stderr=subprocess.STDOUT, stdin=None, stdout=stdout,
cwd=cwd, env=env,
cmd, stderr=subprocess.STDOUT, stdin=subprocess.PIPE,
stdout=stdout, cwd=cwd, env=env,
)
proc.stdin.close()
except Exception as exc:
logger.critical(
"Error %s while executing command %s", exc, command_desc,

View File

@ -17,14 +17,14 @@ import sys
import warnings
from base64 import urlsafe_b64encode
from email.parser import Parser
from sysconfig import get_paths
from pip._vendor import pkg_resources, pytoml
from pip._vendor import pkg_resources
from pip._vendor.distlib.scripts import ScriptMaker
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.six import StringIO
from pip._internal import pep425tags
from pip._internal.build_env import BuildEnvironment
from pip._internal.download import path_to_url, unpack_url
from pip._internal.exceptions import (
InstallationError, InvalidWheelFilename, UnsupportedWheel,
@ -602,58 +602,6 @@ class Wheel(object):
return bool(set(tags).intersection(self.file_tags))
class BuildEnvironment(object):
"""Context manager to install build deps in a simple temporary environment
"""
def __init__(self, no_clean):
self._temp_dir = TempDirectory(kind="build-env")
self._no_clean = no_clean
def __enter__(self):
self._temp_dir.create()
self.save_path = os.environ.get('PATH', None)
self.save_pythonpath = os.environ.get('PYTHONPATH', None)
install_scheme = 'nt' if (os.name == 'nt') else 'posix_prefix'
install_dirs = get_paths(install_scheme, vars={
'base': self._temp_dir.path,
'platbase': self._temp_dir.path,
})
scripts = install_dirs['scripts']
if self.save_path:
os.environ['PATH'] = scripts + os.pathsep + self.save_path
else:
os.environ['PATH'] = scripts + os.pathsep + os.defpath
if install_dirs['purelib'] == install_dirs['platlib']:
lib_dirs = install_dirs['purelib']
else:
lib_dirs = install_dirs['purelib'] + os.pathsep + \
install_dirs['platlib']
if self.save_pythonpath:
os.environ['PYTHONPATH'] = lib_dirs + os.pathsep + \
self.save_pythonpath
else:
os.environ['PYTHONPATH'] = lib_dirs
return self._temp_dir.path
def __exit__(self, exc_type, exc_val, exc_tb):
if not self._no_clean:
self._temp_dir.cleanup()
if self.save_path is None:
os.environ.pop('PATH', None)
else:
os.environ['PATH'] = self.save_path
if self.save_pythonpath is None:
os.environ.pop('PYTHONPATH', None)
else:
os.environ['PYTHONPATH'] = self.save_pythonpath
class WheelBuilder(object):
"""Build wheels from a RequirementSet."""
@ -669,54 +617,13 @@ class WheelBuilder(object):
self.global_options = global_options or []
self.no_clean = no_clean
def _find_build_reqs(self, req):
"""Get a list of the packages required to build the project, if any,
and a flag indicating whether pyproject.toml is present, indicating
that the build should be isolated.
Build requirements can be specified in a pyproject.toml, as described
in PEP 518. If this file exists but doesn't specify build
requirements, pip will default to installing setuptools and wheel.
"""
if os.path.isfile(req.pyproject_toml):
with open(req.pyproject_toml) as f:
pp_toml = pytoml.load(f)
return pp_toml.get('build-system', {})\
.get('requires', ['setuptools', 'wheel']), True
return ['setuptools', 'wheel'], False
def _install_build_reqs(self, reqs, prefix):
# Local import to avoid circular import (wheel <-> req_install)
from pip._internal.req.req_install import InstallRequirement
from pip._internal.index import FormatControl
# Ignore the --no-binary option when installing the build system, so
# we don't recurse trying to build a self-hosting build system.
finder = copy.copy(self.finder)
finder.format_control = FormatControl(set(), set())
urls = [finder.find_requirement(InstallRequirement.from_line(r),
upgrade=False).url
for r in reqs]
args = [sys.executable, '-m', 'pip', 'install', '--ignore-installed',
'--prefix', prefix] + list(urls)
with open_spinner("Installing build dependencies") as spinner:
call_subprocess(args, show_stdout=False, spinner=spinner)
def _build_one(self, req, output_dir, python_tag=None):
"""Build one wheel.
:return: The filename of the built wheel, or None if the build failed.
"""
build_reqs, isolate = self._find_build_reqs(req)
if 'setuptools' not in build_reqs:
logger.warning(
"This version of pip does not implement PEP 516, so "
"it cannot build a wheel without setuptools. You may need to "
"upgrade to a newer version of pip.")
# Install build deps into temporary directory (PEP 518)
with BuildEnvironment(self.no_clean) as prefix:
self._install_build_reqs(build_reqs, prefix)
with req.build_env:
return self._build_one_inside_env(req, output_dir,
python_tag=python_tag,
isolate=True)
@ -806,6 +713,7 @@ class WheelBuilder(object):
buildset = []
for req in requirements:
ephem_cache = False
if req.constraint:
continue
if req.is_wheel:
@ -816,7 +724,8 @@ class WheelBuilder(object):
elif autobuilding and req.editable:
pass
elif autobuilding and req.link and not req.link.is_artifact:
pass
# VCS checkout. Build wheel just for this run.
ephem_cache = True
elif autobuilding and not req.source_dir:
pass
else:
@ -824,9 +733,8 @@ class WheelBuilder(object):
link = req.link
base, ext = link.splitext()
if index.egg_info_matches(base, None, link) is None:
# Doesn't look like a package - don't autobuild a wheel
# because we'll have no way to lookup the result sanely
continue
# E.g. local directory. Build wheel just for this run.
ephem_cache = True
if "binary" not in index.fmt_ctl_formats(
self.finder.format_control,
canonicalize_name(req.name)):
@ -835,7 +743,7 @@ class WheelBuilder(object):
"being disabled for it.", req.name,
)
continue
buildset.append(req)
buildset.append((req, ephem_cache))
if not buildset:
return True
@ -843,15 +751,19 @@ class WheelBuilder(object):
# Build the wheels.
logger.info(
'Building wheels for collected packages: %s',
', '.join([req.name for req in buildset]),
', '.join([req.name for (req, _) in buildset]),
)
_cache = self.wheel_cache # shorter name
with indent_log():
build_success, build_failure = [], []
for req in buildset:
for req, ephem in buildset:
python_tag = None
if autobuilding:
python_tag = pep425tags.implementation_tag
output_dir = self.wheel_cache.get_path_for_link(req.link)
if ephem:
output_dir = _cache.get_ephem_path_for_link(req.link)
else:
output_dir = _cache.get_path_for_link(req.link)
try:
ensure_dir(output_dir)
except OSError as e:

View File

@ -557,18 +557,14 @@ def _get_win_folder_with_jna(csidl_name):
if system == "win32":
try:
import win32com.shell
_get_win_folder = _get_win_folder_with_pywin32
from ctypes import windll
_get_win_folder = _get_win_folder_with_ctypes
except ImportError:
try:
from ctypes import windll
_get_win_folder = _get_win_folder_with_ctypes
import com.sun.jna
_get_win_folder = _get_win_folder_with_jna
except ImportError:
try:
import com.sun.jna
_get_win_folder = _get_win_folder_with_jna
except ImportError:
_get_win_folder = _get_win_folder_from_registry
_get_win_folder = _get_win_folder_from_registry
#---- self test code

View File

@ -1 +0,0 @@
from appdirs import *

View File

@ -38,9 +38,6 @@ import argparse
import subprocess
if not sys.platform.startswith('linux'):
raise ImportError('Unsupported platform: {0}'.format(sys.platform))
_UNIXCONFDIR = os.environ.get('UNIXCONFDIR', '/etc')
_OS_RELEASE_BASENAME = 'os-release'
@ -511,6 +508,21 @@ def distro_release_attr(attribute):
return _distro.distro_release_attr(attribute)
class cached_property(object):
"""A version of @property which caches the value. On access, it calls the
underlying function and sets the value in `__dict__` so future accesses
will not re-call the property.
"""
def __init__(self, f):
self._fname = f.__name__
self._f = f
def __get__(self, obj, owner):
assert obj is not None, 'call {} on an instance'.format(self._fname)
ret = obj.__dict__[self._fname] = self._f(obj)
return ret
class LinuxDistribution(object):
"""
Provides information about a Linux distribution.
@ -576,6 +588,9 @@ class LinuxDistribution(object):
`distro release file`_ that is actually used as a data source. The
empty string if no distro release file is used as a data source.
* ``include_lsb`` (bool): The result of the ``include_lsb`` parameter.
This controls whether the lsb information will be loaded.
Raises:
* :py:exc:`IOError`: Some I/O issue with an os-release file or distro
@ -591,26 +606,20 @@ class LinuxDistribution(object):
self.os_release_file = os_release_file or \
os.path.join(_UNIXCONFDIR, _OS_RELEASE_BASENAME)
self.distro_release_file = distro_release_file or '' # updated later
self._os_release_info = self._get_os_release_info()
self._lsb_release_info = self._get_lsb_release_info() \
if include_lsb else {}
self._distro_release_info = self._get_distro_release_info()
self.include_lsb = include_lsb
def __repr__(self):
"""Return repr of all info
"""
return \
"LinuxDistribution(" \
"os_release_file={0!r}, " \
"distro_release_file={1!r}, " \
"_os_release_info={2!r}, " \
"_lsb_release_info={3!r}, " \
"_distro_release_info={4!r})".format(
self.os_release_file,
self.distro_release_file,
self._os_release_info,
self._lsb_release_info,
self._distro_release_info)
"os_release_file={self.os_release_file!r}, " \
"distro_release_file={self.distro_release_file!r}, " \
"include_lsb={self.include_lsb!r}, " \
"_os_release_info={self._os_release_info!r}, " \
"_lsb_release_info={self._lsb_release_info!r}, " \
"_distro_release_info={self._distro_release_info!r})".format(
self=self)
def linux_distribution(self, full_distribution_name=True):
"""
@ -835,7 +844,8 @@ class LinuxDistribution(object):
"""
return self._distro_release_info.get(attribute, '')
def _get_os_release_info(self):
@cached_property
def _os_release_info(self):
"""
Get the information items from the specified os-release file.
@ -907,34 +917,24 @@ class LinuxDistribution(object):
pass
return props
def _get_lsb_release_info(self):
@cached_property
def _lsb_release_info(self):
"""
Get the information items from the lsb_release command output.
Returns:
A dictionary containing all information items.
"""
cmd = 'lsb_release -a'
process = subprocess.Popen(
cmd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = process.communicate()
stdout, stderr = stdout.decode('utf-8'), stderr.decode('utf-8')
code = process.returncode
if code == 0:
content = stdout.splitlines()
return self._parse_lsb_release_content(content)
elif code == 127: # Command not found
if not self.include_lsb:
return {}
else:
if sys.version_info[:2] >= (3, 5):
raise subprocess.CalledProcessError(code, cmd, stdout, stderr)
elif sys.version_info[:2] >= (2, 7):
raise subprocess.CalledProcessError(code, cmd, stdout)
elif sys.version_info[:2] == (2, 6):
raise subprocess.CalledProcessError(code, cmd)
with open(os.devnull, 'w') as devnull:
try:
cmd = ('lsb_release', '-a')
stdout = subprocess.check_output(cmd, stderr=devnull)
except OSError: # Command not found
return {}
content = stdout.decode(sys.getfilesystemencoding()).splitlines()
return self._parse_lsb_release_content(content)
@staticmethod
def _parse_lsb_release_content(lines):
@ -952,7 +952,6 @@ class LinuxDistribution(object):
"""
props = {}
for line in lines:
line = line.decode('utf-8') if isinstance(line, bytes) else line
kv = line.strip('\n').split(':', 1)
if len(kv) != 2:
# Ignore lines without colon.
@ -961,7 +960,8 @@ class LinuxDistribution(object):
props.update({k.replace(' ', '_').lower(): v.strip()})
return props
def _get_distro_release_info(self):
@cached_property
def _distro_release_info(self):
"""
Get the information items from the specified distro release file.
@ -1001,6 +1001,9 @@ class LinuxDistribution(object):
'fedora-release',
'gentoo-release',
'mageia-release',
'mandrake-release',
'mandriva-release',
'mandrivalinux-release',
'manjaro-release',
'oracle-release',
'redhat-release',

View File

@ -1,6 +1,6 @@
appdirs==1.4.3
distlib==0.2.6
distro==1.0.4
distro==1.2.0
html5lib==1.0b10
six==1.11.0
colorama==0.3.9

View File

@ -144,9 +144,11 @@ def update_stubs(ctx):
print("[vendoring.update_stubs] Add mypy stubs")
# Some projects need stubs other than a simple <name>.pyi
extra_stubs_needed = {
"six": ["six.__init__", "six.moves"]
# Some projects need stubs other than a simple <name>.pyi
"six": ["six.__init__", "six.moves"],
# Some projects should not have stubs coz they're single file modules
"appdirs": [],
}
for lib in vendored_libs:

View File

@ -0,0 +1,28 @@
diff --git a/src/pip/_vendor/appdirs.py b/src/pip/_vendor/appdirs.py
index ae67001a..2bd39110 100644
--- a/src/pip/_vendor/appdirs.py
+++ b/src/pip/_vendor/appdirs.py
@@ -557,18 +557,14 @@ def _get_win_folder_with_jna(csidl_name):
if system == "win32":
try:
- import win32com.shell
- _get_win_folder = _get_win_folder_with_pywin32
+ from ctypes import windll
+ _get_win_folder = _get_win_folder_with_ctypes
except ImportError:
try:
- from ctypes import windll
- _get_win_folder = _get_win_folder_with_ctypes
+ import com.sun.jna
+ _get_win_folder = _get_win_folder_with_jna
except ImportError:
- try:
- import com.sun.jna
- _get_win_folder = _get_win_folder_with_jna
- except ImportError:
- _get_win_folder = _get_win_folder_from_registry
+ _get_win_folder = _get_win_folder_from_registry
#---- self test code

View File

@ -1,2 +1,2 @@
[build-system]
requires=["simple==3.0", "setuptools", "wheel"]
requires=["simplewheel==2.0", "setuptools", "wheel"]

View File

@ -1,6 +1,8 @@
#!/usr/bin/env python
from setuptools import find_packages, setup
import simple # ensure dependency is installed
setup(name='pep518',
version='3.0',
packages=find_packages()

View File

@ -994,16 +994,15 @@ def test_install_builds_wheels(script, data, common_wheels):
# and built wheels for upper and wheelbroken
assert "Running setup.py bdist_wheel for upper" in str(res), str(res)
assert "Running setup.py bdist_wheel for wheelb" in str(res), str(res)
# But not requires_wheel... which is a local dir and thus uncachable.
assert "Running setup.py bdist_wheel for requir" not in str(res), str(res)
# Wheels are built for local directories, but not cached.
assert "Running setup.py bdist_wheel for requir" in str(res), str(res)
# wheelbroken has to run install
# into the cache
assert wheels != [], str(res)
# and installed from the wheel
assert "Running setup.py install for upper" not in str(res), str(res)
# the local tree can't build a wheel (because we can't assume that every
# build will have a suitable unique key to cache on).
assert "Running setup.py install for requires-wheel" in str(res), str(res)
# Wheels are built for local directories, but not cached.
assert "Running setup.py install for requir" not in str(res), str(res)
# wheelbroken has to run install
assert "Running setup.py install for wheelb" in str(res), str(res)
# We want to make sure we used the correct implementation tag
@ -1027,13 +1026,12 @@ def test_install_no_binary_disables_building_wheels(
assert expected in str(res), str(res)
# and built wheels for wheelbroken only
assert "Running setup.py bdist_wheel for wheelb" in str(res), str(res)
# But not requires_wheel... which is a local dir and thus uncachable.
assert "Running setup.py bdist_wheel for requir" not in str(res), str(res)
# Nor upper, which was blacklisted
# Wheels are built for local directories, but not cached across runs
assert "Running setup.py bdist_wheel for requir" in str(res), str(res)
# Don't build wheel for upper which was blacklisted
assert "Running setup.py bdist_wheel for upper" not in str(res), str(res)
# the local tree can't build a wheel (because we can't assume that every
# build will have a suitable unique key to cache on).
assert "Running setup.py install for requires-wheel" in str(res), str(res)
# Wheels are built for local directories, but not cached across runs
assert "Running setup.py install for requir" not in str(res), str(res)
# And these two fell back to sdist based installed.
assert "Running setup.py install for wheelb" in str(res), str(res)
assert "Running setup.py install for upper" in str(res), str(res)
@ -1204,10 +1202,10 @@ def test_basic_install_environment_markers(script):
)
"""))
res = script.pip('install', '--no-index', pkga_path, expect_stderr=True)
res = script.pip('install', '--no-index', pkga_path)
# missing_pkg should be ignored
assert ("Ignoring missing-pkg: markers 'python_version == \"1.0\"' don't "
"match your environment") in res.stderr, str(res)
"match your environment") in res.stdout, str(res)
assert "Successfully installed pkga-0.1" in res.stdout, str(res)

View File

@ -31,7 +31,7 @@ def test_no_clean_option_blocks_cleaning_after_install(script, data):
build = script.base_path / 'pip-build'
script.pip(
'install', '--no-clean', '--no-index', '--build', build,
'--find-links=%s' % data.find_links, 'simple',
'--find-links=%s' % data.find_links, 'simple', expect_temp=True,
)
assert exists(build)
@ -134,7 +134,7 @@ def test_cleanup_prevented_upon_build_dir_exception(script, data):
result = script.pip(
'install', '-f', data.find_links, '--no-index', 'simple',
'--build', build,
expect_error=True,
expect_error=True, expect_temp=True,
)
assert result.returncode == PREVIOUS_BUILD_DIR_ERROR

View File

@ -300,6 +300,14 @@ def test_constraints_local_editable_install_causes_error(script, data):
assert 'Could not satisfy constraints for' in result.stderr
def test_constraints_local_editable_install_pep518(script, data):
to_install = data.src.join("pep518-3.0")
script.pip('download', 'setuptools', 'wheel', '-d', data.packages)
script.pip(
'install', '--no-index', '-f', data.find_links, '-e', to_install)
def test_constraints_local_install_causes_error(script, data):
script.scratch_path.join("constraints.txt").write(
"singlemodule==0.0.0"
@ -484,11 +492,10 @@ def test_install_unsupported_wheel_link_with_marker(script):
result = script.pip(
'install', '-r', script.scratch_path / 'with-marker.txt',
expect_error=False,
expect_stderr=True,
)
assert ("Ignoring asdf: markers 'sys_platform == \"xyz\"' don't match "
"your environment") in result.stderr
"your environment") in result.stdout
assert len(result.files_created) == 0

View File

@ -68,24 +68,29 @@ class Tests_UserSite:
)
result.assert_installed('INITools', use_user_site=True)
def test_install_curdir_usersite(self, script, virtualenv, data):
@pytest.mark.network
def test_install_from_current_directory_into_usersite(
self, script, virtualenv, data, common_wheels):
"""
Test installing current directory ('.') into usersite
"""
virtualenv.system_site_packages = True
script.pip("install", "wheel", '--no-index', '-f', common_wheels)
run_from = data.packages.join("FSPkg")
result = script.pip(
'install', '-vvv', '--user', curdir,
cwd=run_from,
expect_error=False,
)
fspkg_folder = script.user_site / 'fspkg'
egg_info_folder = (
script.user_site / 'FSPkg-0.1.dev0-py%s.egg-info' % pyversion
)
assert fspkg_folder in result.files_created, result.stdout
assert egg_info_folder in result.files_created
dist_info_folder = (
script.user_site / 'FSPkg-0.1.dev0.dist-info'
)
assert dist_info_folder in result.files_created
def test_install_user_venv_nositepkgs_fails(self, script, data):
"""

View File

@ -193,7 +193,7 @@ def test_pip_wheel_fail_cause_of_previous_build_dir(
result = script.pip(
'wheel', '--no-index', '--find-links=%s' % data.find_links,
'--build', script.venv_path / 'build',
'simple==3.0', expect_error=True,
'simple==3.0', expect_error=True, expect_temp=True,
)
# Then I see that the error code is the right one

View File

@ -495,6 +495,58 @@ class TestParseRequirements(object):
assert finder.index_urls == ['Good']
def test_expand_existing_env_variables(self, tmpdir, finder):
template = (
'https://%s:x-oauth-basic@github.com/user/%s/archive/master.zip'
)
env_vars = (
('GITHUB_TOKEN', 'notarealtoken'),
('DO_12_FACTOR', 'awwyeah'),
)
with open(tmpdir.join('req1.txt'), 'w') as fp:
fp.write(template % tuple(['${%s}' % k for k, _ in env_vars]))
with patch('pip._internal.req.req_file.os.getenv') as getenv:
getenv.side_effect = lambda n: dict(env_vars)[n]
reqs = list(parse_requirements(
tmpdir.join('req1.txt'),
finder=finder,
session=PipSession()
))
assert len(reqs) == 1, \
'parsing requirement file with env variable failed'
expected_url = template % tuple([v for _, v in env_vars])
assert reqs[0].link.url == expected_url, \
'variable expansion in req file failed'
def test_expand_missing_env_variables(self, tmpdir, finder):
req_url = (
'https://${NON_EXISTENT_VARIABLE}:$WRONG_FORMAT@'
'%WINDOWS_FORMAT%github.com/user/repo/archive/master.zip'
)
with open(tmpdir.join('req1.txt'), 'w') as fp:
fp.write(req_url)
with patch('pip._internal.req.req_file.os.getenv') as getenv:
getenv.return_value = ''
reqs = list(parse_requirements(
tmpdir.join('req1.txt'),
finder=finder,
session=PipSession()
))
assert len(reqs) == 1, \
'parsing requirement file with env variable failed'
assert reqs[0].link.url == req_url, \
'ignoring invalid env variable in req file failed'
def test_join_lines(self, tmpdir, finder):
with open(tmpdir.join("req1.txt"), "w") as fp:
fp.write("--extra-index-url url1 \\\n--extra-index-url url2")

View File

@ -23,8 +23,8 @@ from pip._internal.utils.encoding import auto_decode
from pip._internal.utils.glibc import check_glibc_version
from pip._internal.utils.hashes import Hashes, MissingHashes
from pip._internal.utils.misc import (
egg_link_path, ensure_dir, get_installed_distributions, get_prog,
normalize_path, rmtree, untar_file, unzip_file,
call_subprocess, egg_link_path, ensure_dir, get_installed_distributions,
get_prog, normalize_path, rmtree, untar_file, unzip_file,
)
from pip._internal.utils.packaging import check_dist_requires_python
from pip._internal.utils.temp_dir import TempDirectory
@ -612,3 +612,15 @@ class TestGetProg(object):
executable
)
assert get_prog() == expected
def test_call_subprocess_works_okay_when_just_given_nothing():
try:
call_subprocess([sys.executable, '-c', 'print("Hello")'])
except Exception:
assert False, "Expected subprocess call to succeed"
def test_call_subprocess_closes_stdin():
with pytest.raises(InstallationError):
call_subprocess([sys.executable, '-c', 'input()'])