Merge branch 'master' into resolver/move-dependency-info-and-install-order

This commit is contained in:
Pradyun Gedam 2018-03-03 02:06:12 +05:30
commit 6caca65942
No known key found for this signature in database
GPG Key ID: DA17C4B29CB32E4B
24 changed files with 382 additions and 180 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
++++++++++++++++

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.

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/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

@ -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

@ -344,7 +344,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:

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

@ -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,14 @@ 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):
def prepare_editable_requirement(self, req, require_hashes, finder):
"""Prepare an editable requirement
"""
assert req.editable, "cannot prepare a non-editable req as editable"
@ -284,7 +331,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,
@ -128,6 +129,7 @@ class InstallRequirement(object):
self.prepared = False
self.isolated = isolated
self.build_env = BuildEnvironment(no_clean=True)
@classmethod
def from_editable(cls, editable_req, comes_from=None, isolated=False,
@ -452,6 +454,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:
@ -864,6 +882,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):
@ -878,19 +897,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

@ -57,16 +57,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 [], None
# This check has to come after we filter requirements with the

View File

@ -191,7 +191,9 @@ class Resolver(object):
if req.editable:
return self.preparer.prepare_editable_requirement(
req, self.require_hashes
req,
self.require_hashes,
self.finder,
)
# satisfied_by is only evaluated by calling _check_skip_installed,
@ -250,11 +252,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

@ -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)

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

@ -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

@ -1202,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

@ -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

@ -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")