Merge pull request #4248 from pypa/bugfix/4231

This commit is contained in:
Dan Ryan
2020-05-11 10:29:53 -04:00
committed by GitHub
21 changed files with 576 additions and 19 deletions
+2
View File
@@ -0,0 +1,2 @@
Fixed a bug which caused pipenv to prefer source distributions over wheels from ``PyPI`` during the dependency resolution phase.
Fixed an issue which prevented proper build isolation using ``pep517`` based builders during dependency resolution.
+54 -1
View File
@@ -14,6 +14,25 @@ from .vendor.vistir.misc import _isatty, fs_str
# HACK: avoid resolver.py uses the wrong byte code files.
# I hope I can remove this one day.
os.environ["PYTHONDONTWRITEBYTECODE"] = fs_str("1")
_false_values = ("0", "false", "no", "off")
_true_values = ("1", "true", "yes", "on")
def env_to_bool(val):
"""
Convert **val** to boolean, returning True if truthy or False if falsey
:param Any val: The value to convert
:return: False if Falsey, True if truthy
:rtype: bool
"""
if isinstance(val, bool):
return val
if val.lower() in _false_values:
return False
if val.lower() in _true_values:
return True
raise ValueError("Value is not a valid boolean-like: {0}".format(val))
def _is_env_truthy(name):
@@ -21,7 +40,41 @@ def _is_env_truthy(name):
"""
if name not in os.environ:
return False
return os.environ.get(name).lower() not in ("0", "false", "no", "off")
return os.environ.get(name).lower() not in _false_values
def get_from_env(arg, prefix="PIPENV", check_for_negation=True):
"""
Check the environment for a variable, returning its truthy or stringified value
For example, setting ``PIPENV_NO_RESOLVE_VCS=1`` would mean that
``get_from_env("RESOLVE_VCS", prefix="PIPENV")`` would return ``False``.
:param str arg: The name of the variable to look for
:param str prefix: The prefix to attach to the variable, defaults to "PIPENV"
:param bool check_for_negation: Whether to check for ``<PREFIX>_NO_<arg>``, defaults
to True
:return: The value from the environment if available
:rtype: Optional[Union[str, bool]]
"""
negative_lookup = "NO_{0}".format(arg)
positive_lookup = arg
if prefix:
positive_lookup = "{0}_{1}".format(prefix, arg)
negative_lookup = "{0}_{1}".format(prefix, negative_lookup)
if positive_lookup in os.environ:
value = os.environ[positive_lookup]
try:
return env_to_bool(value)
except ValueError:
return value
if check_for_negation and negative_lookup in os.environ:
value = os.environ[negative_lookup]
try:
return not env_to_bool(value)
except ValueError:
return value
return None
PIPENV_IS_CI = bool("CI" in os.environ or "TF_BUILD" in os.environ)
+2
View File
@@ -11,7 +11,9 @@ if __package__ == '':
# Resulting path is the name of the wheel itself
# Add that to sys.path so we can import pipenv.patched.notpip
path = os.path.dirname(os.path.dirname(__file__))
pipenv = os.path.dirname(os.path.dirname(path))
sys.path.insert(0, path)
sys.path.insert(0, pipenv)
from pipenv.patched.notpip._internal.cli.main import main as _main # isort:skip # noqa
@@ -535,7 +535,7 @@ class CandidateEvaluator(object):
)
if self._prefer_binary:
binary_preference = 1
tags = self.valid_tags if not ignore_compatibility else None
tags = valid_tags
try:
pri = -(wheel.support_index_min(tags=tags))
except TypeError:
+1 -1
View File
@@ -76,7 +76,7 @@ def simplify_markers(ireq):
def clean_requires_python(candidates):
"""Get a cleaned list of all the candidates with valid specifiers in the `requires_python` attributes."""
all_candidates = []
py_version = parse_version(os.environ.get('PIP_PYTHON_VERSION', '.'.join(map(str, sys.version_info[:3]))))
py_version = parse_version(os.environ.get('PIPENV_REQUESTED_PYTHON_VERSION', '.'.join(map(str, sys.version_info[:3]))))
for c in candidates:
if getattr(c, "requires_python", None):
# Old specifications had people setting this to single digits
+1 -1
View File
@@ -771,7 +771,7 @@ def resolve_packages(pre, clear, verbose, system, write, requirements_dir, packa
def _main(pre, clear, verbose, system, write, requirements_dir, packages, parse_only=False):
os.environ["PIP_PYTHON_VERSION"] = ".".join([str(s) for s in sys.version_info[:3]])
os.environ["PIPENV_REQUESTED_PYTHON_VERSION"] = ".".join([str(s) for s in sys.version_info[:3]])
os.environ["PIP_PYTHON_PATH"] = str(sys.executable)
if parse_only:
parse_packages(
+8 -11
View File
@@ -236,14 +236,14 @@ class HackedPythonVersion(object):
def __enter__(self):
# Only inject when the value is valid
if self.python_version:
os.environ["PIP_PYTHON_VERSION"] = str(self.python_version)
os.environ["PIPENV_REQUESTED_PYTHON_VERSION"] = str(self.python_version)
if self.python_path:
os.environ["PIP_PYTHON_PATH"] = str(self.python_path)
def __exit__(self, *args):
# Restore original Python version information.
try:
del os.environ["PIP_PYTHON_VERSION"]
del os.environ["PIPENV_REQUESTED_PYTHON_VERSION"]
except KeyError:
pass
@@ -682,25 +682,21 @@ class Resolver(object):
self._pip_command = self._get_pip_command()
return self._pip_command
def prepare_pip_args(self, use_pep517=True, build_isolation=True):
def prepare_pip_args(self, use_pep517=False, build_isolation=True):
pip_args = []
if self.sources:
pip_args = prepare_pip_source_args(self.sources, pip_args)
if not use_pep517:
if use_pep517 is False:
pip_args.append("--no-use-pep517")
if not build_isolation:
if build_isolation is False:
pip_args.append("--no-build-isolation")
pip_args.extend(["--cache-dir", environments.PIPENV_CACHE_DIR])
return pip_args
@property
def pip_args(self):
use_pep517 = False if (
os.environ.get("PIP_NO_USE_PEP517", None) is not None
) else (True if os.environ.get("PIP_USE_PEP517", None) is not None else None)
build_isolation = False if (
os.environ.get("PIP_NO_BUILD_ISOLATION", None) is not None
) else (True if os.environ.get("PIP_BUILD_ISOLATION", None) is not None else None)
use_pep517 = environments.get_from_env("USE_PEP517", prefix="PIP")
build_isolation = environments.get_from_env("BUILD_ISOLATION", prefix="PIP")
if self._pip_args is None:
self._pip_args = self.prepare_pip_args(
use_pep517=use_pep517, build_isolation=build_isolation
@@ -790,6 +786,7 @@ class Resolver(object):
self._resolver = PiptoolsResolver(
constraints=self.parsed_constraints, repository=self.repository,
cache=DependencyCache(environments.PIPENV_CACHE_DIR), clear_caches=clear,
# TODO: allow users to toggle the 'allow unsafe' flag to resolve setuptools?
prereleases=pre, allow_unsafe=False
)
+16 -2
View File
@@ -114,7 +114,7 @@ index 02a187c8..f917e645 100644
modifying_pip=modifying_pip
)
diff --git a/pipenv/patched/pip/_internal/index/package_finder.py b/pipenv/patched/pip/_internal/index/package_finder.py
index a74d78db..11128f4d 100644
index a74d78db..7c9dc1be 100644
--- a/pipenv/patched/pip/_internal/index/package_finder.py
+++ b/pipenv/patched/pip/_internal/index/package_finder.py
@@ -121,6 +121,7 @@ class LinkEvaluator(object):
@@ -201,7 +201,7 @@ index a74d78db..11128f4d 100644
if self._prefer_binary:
binary_preference = 1
- pri = -(wheel.support_index_min(valid_tags))
+ tags = self.valid_tags if not ignore_compatibility else None
+ tags = valid_tags
+ try:
+ pri = -(wheel.support_index_min(tags=tags))
+ except TypeError:
@@ -589,3 +589,17 @@ index 65e41bc7..9eabf28e 100644
class AdjacentTempDirectory(TempDirectory):
diff --git a/pipenv/patched/pip/__main__.py b/pipenv/patched/pip/__main__.py
index 56f669fa..3c216189 100644
--- a/pipenv/patched/pip/__main__.py
+++ b/pipenv/patched/pip/__main__.py
@@ -11,7 +11,9 @@ if __package__ == '':
# Resulting path is the name of the wheel itself
# Add that to sys.path so we can import pip
path = os.path.dirname(os.path.dirname(__file__))
+ pipenv = os.path.dirname(os.path.dirname(path))
sys.path.insert(0, path)
+ sys.path.insert(0, pipenv)
from pip._internal.cli.main import main as _main # isort:skip # noqa
@@ -745,7 +745,7 @@ index 7733447..e6f232f 100644
+def clean_requires_python(candidates):
+ """Get a cleaned list of all the candidates with valid specifiers in the `requires_python` attributes."""
+ all_candidates = []
+ py_version = parse_version(os.environ.get('PIP_PYTHON_VERSION', '.'.join(map(str, sys.version_info[:3]))))
+ py_version = parse_version(os.environ.get('PIPENV_REQUESTED_PYTHON_VERSION', '.'.join(map(str, sys.version_info[:3]))))
+ for c in candidates:
+ if getattr(c, "requires_python", None):
+ # Old specifications had people setting this to single digits
+52
View File
@@ -0,0 +1,52 @@
[build-system]
requires = ["setuptools >= 40.6.0", "setuptools-scm", "cython"]
build-backend = "setuptools.build_meta"
[tool.black]
line-length = 90
target_version = ['py27', 'py35', 'py36', 'py37', 'py38']
include = '\.pyi?$'
exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.pyre_configuration
| \.venv
| _build
| buck-out
| build
| dist
)
'''
[tool.towncrier]
package = 'cython-import-package'
package_dir = 'src'
filename = 'CHANGELOG.rst'
directory = 'news/'
title_format = '{version} ({project_date})'
issue_format = '`#{issue} <https://github.com/sarugaku/cython_import_package/issues/{issue}>`_'
template = 'tasks/CHANGELOG.rst.jinja2'
[[tool.towncrier.type]]
directory = 'feature'
name = 'Features'
showcontent = true
[[tool.towncrier.type]]
directory = 'bugfix'
name = 'Bug Fixes'
showcontent = true
[[tool.towncrier.type]]
directory = 'trivial'
name = 'Trivial Changes'
showcontent = false
[[tool.towncrier.type]]
directory = 'removal'
name = 'Removals and Deprecations'
showcontent = true
+58
View File
@@ -0,0 +1,58 @@
[metadata]
name = cython_import_package
package_name = cython-import-package
description = A fake python package.
url = https://github.com/sarugaku/cython_import_package
author = Dan Ryan
author_email = dan@danryan.co
long_description = file: README.rst
license = ISC License
keywords = fake package test
classifier =
Development Status :: 1 - Planning
License :: OSI Approved :: ISC License (ISCL)
Operating System :: OS Independent
Programming Language :: Python :: 2
Programming Language :: Python :: 2.6
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.4
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Topic :: Software Development :: Libraries :: Python Modules
[options.extras_require]
tests =
pytest
pytest-xdist
pytest-cov
pytest-timeout
readme-renderer[md]
twine
dev =
black;python_version>="3.6"
flake8
flake8-bugbear;python_version>="3.5"
invoke
isort
mypy;python_version>="3.5"
parver
pre-commit
rope
wheel
[options]
zip_safe = true
python_requires = >=2.6,!=3.0,!=3.1,!=3.2,!=3.3
install_requires =
attrs
vistir
[bdist_wheel]
universal = 1
[egg_info]
tag_build =
tag_date = 0
+43
View File
@@ -0,0 +1,43 @@
import ast
import os
from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
# ORDER MATTERS
# Import this after setuptools or it will fail
from Cython.Build import cythonize # noqa: I100
import Cython.Distutils
ROOT = os.path.dirname(__file__)
PACKAGE_NAME = 'cython_import_package'
VERSION = None
with open(os.path.join(ROOT, 'src', PACKAGE_NAME.replace("-", "_"), '__init__.py')) as f:
for line in f:
if line.startswith('__version__ = '):
VERSION = ast.literal_eval(line[len('__version__ = '):].strip())
break
if VERSION is None:
raise EnvironmentError('failed to read version')
# Put everything in setup.cfg, except those that don't actually work?
setup(
# These really don't work.
package_dir={'': 'src'},
packages=find_packages('src'),
# I don't know how to specify an empty key in setup.cfg.
package_data={
'': ['LICENSE*', 'README*'],
},
setup_requires=["setuptools_scm", "cython"],
# I need this to be dynamic.
version=VERSION,
)
@@ -0,0 +1 @@
__version__ = "0.0.1"
+51
View File
@@ -0,0 +1,51 @@
[build-system]
requires = ["setuptools>=30.3.0", "wheel", "setuptools_scm>=3.3.1"]
[tool.black]
line-length = 90
target_version = ['py27', 'py35', 'py36', 'py37', 'py38']
include = '\.pyi?$'
exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.pyre_configuration
| \.venv
| _build
| buck-out
| build
| dist
)
'''
[tool.towncrier]
package = 'legacy-backend-package'
package_dir = 'src'
filename = 'CHANGELOG.rst'
directory = 'news/'
title_format = '{version} ({project_date})'
issue_format = '`#{issue} <https://github.com/sarugaku/legacy_backend_package/issues/{issue}>`_'
template = 'tasks/CHANGELOG.rst.jinja2'
[[tool.towncrier.type]]
directory = 'feature'
name = 'Features'
showcontent = true
[[tool.towncrier.type]]
directory = 'bugfix'
name = 'Bug Fixes'
showcontent = true
[[tool.towncrier.type]]
directory = 'trivial'
name = 'Trivial Changes'
showcontent = false
[[tool.towncrier.type]]
directory = 'removal'
name = 'Removals and Deprecations'
showcontent = true
+127
View File
@@ -0,0 +1,127 @@
[metadata]
name = legacy_backend_package
package_name = legacy-backend-package
description = A fake python package.
url = https://github.com/sarugaku/legacy_backend_package
author = Dan Ryan
author_email = dan@danryan.co
long_description = file: README.rst
license = ISC License
keywords = fake package test
classifier =
Development Status :: 1 - Planning
License :: OSI Approved :: ISC License (ISCL)
Operating System :: OS Independent
Programming Language :: Python :: 2
Programming Language :: Python :: 2.6
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.4
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Topic :: Software Development :: Libraries :: Python Modules
[options.extras_require]
tests =
pytest
pytest-xdist
pytest-cov
pytest-timeout
readme-renderer[md]
twine
dev =
black;python_version>="3.6"
flake8
flake8-bugbear;python_version>="3.5"
invoke
isort
mypy;python_version>="3.5"
parver
pre-commit
rope
wheel
[options]
zip_safe = true
python_requires = >=2.6,!=3.0,!=3.1,!=3.2,!=3.3
setup_requires =
setuptools_scm>=3.3.1
install_requires =
attrs
vistir
[bdist_wheel]
universal = 1
[egg_info]
tag_build =
tag_date = 0
[tool:pytest]
strict = true
plugins = cov flake8
addopts = -ra
testpaths = tests/
norecursedirs = .* build dist news tasks docs
flake8-ignore =
docs/source/* ALL
tests/*.py ALL
setup.py ALL
filterwarnings =
ignore::DeprecationWarning
ignore::PendingDeprecationWarning
[isort]
atomic = true
not_skip = __init__.py
line_length = 90
indent = ' '
multi_line_output = 3
known_third_party = invoke,parver,pytest,setuptools,towncrier
known_first_party =
legacy_backend_package
tests
combine_as_imports=True
include_trailing_comma = True
force_grid_wrap=0
[flake8]
max-line-length = 90
select = C,E,F,W,B,B950
ignore =
# The default ignore list:
D203,F401,E123,E203,W503,E501,E402
#E121,E123,E126,E226,E24,E704,
# Our additions:
# E123: closing bracket does not match indentation of opening brackets line
# E203: whitespace before :
# E129: visually indented line with same indent as next logical line
# E222: multiple spaces after operator
# E231: missing whitespace after ','
# D203: 1 blank line required before class docstring
# E402: module level import not at top of file
# E501: line too long (using B950 from flake8-bugbear)
# F401: Module imported but unused
# W503: line break before binary operator (not a pep8 issue, should be ignored)
exclude =
.tox,
.git,
__pycache__,
docs/source/*,
build,
dist,
tests/*,
*.pyc,
*.egg-info,
.cache,
.eggs,
setup.py,
max-complexity=13
[mypy]
ignore_missing_imports=true
follow_imports=skip
html_report=mypyhtml
python_version=2.7
+35
View File
@@ -0,0 +1,35 @@
import ast
import os
from setuptools import setup, find_packages
from setuptools.command.test import test as TestCommand
ROOT = os.path.dirname(__file__)
PACKAGE_NAME = 'legacy_backend_package'
VERSION = None
with open(os.path.join(ROOT, 'src', PACKAGE_NAME.replace("-", "_"), '__init__.py')) as f:
for line in f:
if line.startswith('__version__ = '):
VERSION = ast.literal_eval(line[len('__version__ = '):].strip())
break
if VERSION is None:
raise EnvironmentError('failed to read version')
# Put everything in setup.cfg, except those that don't actually work?
setup(
# These really don't work.
package_dir={'': 'src'},
packages=find_packages('src'),
# I don't know how to specify an empty key in setup.cfg.
package_data={
'': ['LICENSE*', 'README*'],
},
# I need this to be dynamic.
version=VERSION,
)
@@ -0,0 +1 @@
__version__ = "0.0.1"
+2
View File
@@ -133,6 +133,8 @@ def pytest_runtest_setup(item):
sys.version_info[:2] == (3, 6)
):
pytest.skip('test is skipped on python 3.6')
if item.get_closest_marker('skip_windows') is not None and (os.name == 'nt'):
pytest.skip('test does not run on windows')
@pytest.fixture
+51
View File
@@ -384,6 +384,57 @@ fake-package = "*"
assert '--extra-index-url {}'.format(mirror_url) not in c.out.strip()
@pytest.mark.lock
@pytest.mark.install
@pytest.mark.skip_windows
@pytest.mark.needs_internet
def test_outdated_setuptools_with_pep517_legacy_build_meta_is_updated(PipenvInstance):
"""
This test ensures we are using build isolation and a pep517 backend
because the package in question includes ``pyproject.toml`` but lacks
a ``build-backend`` declaration. In this case, ``pip`` defaults to using
``setuptools.build_meta:__legacy__`` as a builder, but without ``pep517``
enabled and with ``setuptools==40.2.0`` installed, this build backend was
not yet available. ``setuptools<40.8`` will not be aware of this backend.
If pip is able to build in isolation with a pep517 backend, this will not
matter and the test will still pass as pip will by default install a more
recent version of ``setuptools``.
"""
with PipenvInstance(chdir=True) as p:
c = p.pipenv('run pip install "setuptools<=40.2"')
assert c.return_code == 0
c = p.pipenv("run python -c 'import setuptools; print(setuptools.__version__)'")
assert c.return_code == 0
assert c.out.strip() == "40.2.0"
c = p.pipenv("install legacy-backend-package")
assert c.return_code == 0
assert "vistir" in p.lockfile["default"]
@pytest.mark.lock
@pytest.mark.install
@pytest.mark.skip_windows
@pytest.mark.needs_internet
def test_outdated_setuptools_with_pep517_cython_import_in_setuppy(PipenvInstance):
"""
This test ensures we are using build isolation and a pep517 backend
because the package in question declares 'cython' as a build dependency
in ``pyproject.toml``, then imports it in ``setup.py``. The pep517
backend will have to install it first, so this will only pass if the
resolver is buliding with a proper backend.
"""
with PipenvInstance(chdir=True) as p:
c = p.pipenv('run pip install "setuptools<=40.2"')
assert c.return_code == 0
c = p.pipenv("run python -c 'import setuptools; print(setuptools.__version__)'")
assert c.return_code == 0
assert c.out.strip() == "40.2.0"
c = p.pipenv("install cython-import-package")
assert c.return_code == 0
assert "vistir" in p.lockfile["default"]
@pytest.mark.index
@pytest.mark.install
def test_lock_updated_source(PipenvInstance):
+68
View File
@@ -0,0 +1,68 @@
import itertools
import pytest
import os
from pipenv import environments
from pipenv.utils import temp_environ
@pytest.mark.environments
@pytest.mark.parametrize(
"arg, prefix, use_negation",
list(itertools.product(("ENABLE_SOMETHING",), ("FAKEPREFIX", None), (True, False))),
)
def test_get_from_env(arg, prefix, use_negation):
negated_arg = "NO_{0}".format(arg)
positive_var = arg
negative_var = negated_arg
if prefix:
negative_var = "{0}_{1}".format(prefix, negative_var)
positive_var = "{0}_{1}".format(prefix, positive_var)
# set the positive first
for var_to_set, opposite_var in ((arg, negated_arg), (negated_arg, arg)):
os.environ.pop(var_to_set, None)
os.environ.pop(opposite_var, None)
with temp_environ():
is_positive = var_to_set == arg
is_negative = not is_positive
envvar = positive_var if is_positive else negative_var
os.environ[envvar] = "true"
main_expected_value = True if is_positive else None
if use_negation and not is_positive:
main_expected_value = False
# use negation means if the normal variable isnt set we will check
# for the negated version
negative_expected_value = (
True if is_negative else None
)
if is_positive:
assert (
environments.get_from_env(
var_to_set, prefix, check_for_negation=use_negation
)
is main_expected_value
)
assert (
environments.get_from_env(
opposite_var, prefix, check_for_negation=use_negation
)
is negative_expected_value
)
else:
# var_to_set = negative version i.e. NO_xxxx
# opposite_var = positive_version i.e. XXXX
# get NO_BLAH -- expecting this to be True
assert (
environments.get_from_env(
var_to_set, prefix, check_for_negation=use_negation
)
is negative_expected_value
)
# get BLAH -- expecting False if checking for negation
# but otherwise should be None
assert (
environments.get_from_env(
opposite_var, prefix, check_for_negation=use_negation
)
is main_expected_value
)