diff --git a/news/1901.bugfix b/news/1901.bugfix new file mode 100644 index 00000000..7106fe65 --- /dev/null +++ b/news/1901.bugfix @@ -0,0 +1 @@ +Fixed an ongoing bug which sometimes resolved incompatible versions into lockfiles. diff --git a/pipenv/patched/piptools/utils.py b/pipenv/patched/piptools/utils.py index 66bfafa6..0ee22a56 100644 --- a/pipenv/patched/piptools/utils.py +++ b/pipenv/patched/piptools/utils.py @@ -12,6 +12,7 @@ from ._compat import InstallRequirement from first import first from pipenv.patched.notpip._vendor.packaging.specifiers import SpecifierSet, InvalidSpecifier +from pipenv.patched.notpip._vendor.packaging.version import Version, InvalidVersion, parse as parse_version from .click import style @@ -21,6 +22,7 @@ UNSAFE_PACKAGES = {'setuptools', 'distribute', 'pip'} 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])))) for c in candidates: if c.requires_python: # Old specifications had people setting this to single digits @@ -28,9 +30,12 @@ def clean_requires_python(candidates): if c.requires_python.isdigit(): c.requires_python = '>={0},<{1}'.format(c.requires_python, int(c.requires_python) + 1) try: - SpecifierSet(c.requires_python) + specifierset = SpecifierSet(c.requires_python) except InvalidSpecifier: continue + else: + if not specifierset.contains(py_version): + continue all_candidates.append(c) return all_candidates diff --git a/pipenv/resolver.py b/pipenv/resolver.py index 337ef22b..91259bd2 100644 --- a/pipenv/resolver.py +++ b/pipenv/resolver.py @@ -35,7 +35,8 @@ def main(): sys.argv = new_sys_argv from pipenv.utils import create_mirror_source, resolve_deps, replace_pypi_sources - + os.environ['PIP_PYTHON_VERSION'] = '.'.join([str(s) for s in sys.version_info[:3]]) + os.environ['PIP_PYTHON_PATH'] = sys.executable if is_verbose: logging.getLogger('notpip').setLevel(logging.INFO) if is_debug: diff --git a/pipenv/utils.py b/pipenv/utils.py index 2068a227..40799881 100644 --- a/pipenv/utils.py +++ b/pipenv/utils.py @@ -173,12 +173,18 @@ class HackedPythonVersion(object): self.python_path = python_path def __enter__(self): - os.environ['PIP_PYTHON_VERSION'] = str(self.python_version) - os.environ['PIP_PYTHON_PATH'] = str(self.python_path) + # Only inject when the value is valid + if self.python_version: + os.environ['PIP_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. - del os.environ['PIP_PYTHON_VERSION'] + try: + del os.environ['PIP_PYTHON_VERSION'] + except KeyError: + pass def prepare_pip_source_args(sources, pip_args=None): diff --git a/tasks/vendoring/patches/patched/piptools.patch b/tasks/vendoring/patches/patched/piptools.patch index 7e864fdf..b1ba98a5 100644 --- a/tasks/vendoring/patches/patched/piptools.patch +++ b/tasks/vendoring/patches/patched/piptools.patch @@ -541,15 +541,16 @@ index 08dabe1..480ad1e 100644 else: return self.repository.find_best_match(ireq, prereleases) diff --git a/pipenv/patched/piptools/utils.py b/pipenv/patched/piptools/utils.py -index fde5816..5827a55 100644 +index fde5816..fb71882 100644 --- a/pipenv/patched/piptools/utils.py +++ b/pipenv/patched/piptools/utils.py -@@ -11,13 +11,30 @@ from contextlib import contextmanager +@@ -11,13 +11,35 @@ from contextlib import contextmanager from ._compat import InstallRequirement from first import first - +from pip._vendor.packaging.specifiers import SpecifierSet, InvalidSpecifier ++from pip._vendor.packaging.version import Version, InvalidVersion, parse as parse_version from .click import style @@ -559,6 +560,7 @@ index fde5816..5827a55 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])))) + for c in candidates: + if c.requires_python: + # Old specifications had people setting this to single digits @@ -566,9 +568,12 @@ index fde5816..5827a55 100644 + if c.requires_python.isdigit(): + c.requires_python = '>={0},<{1}'.format(c.requires_python, int(c.requires_python) + 1) + try: -+ SpecifierSet(c.requires_python) ++ specifierset = SpecifierSet(c.requires_python) + except InvalidSpecifier: + continue ++ else: ++ if not specifierset.contains(py_version): ++ continue + all_candidates.append(c) + return all_candidates + @@ -576,7 +581,7 @@ index fde5816..5827a55 100644 def key_from_ireq(ireq): """Get a standardized key for an InstallRequirement.""" if ireq.req is None and ireq.link is not None: -@@ -43,16 +60,51 @@ def comment(text): +@@ -43,16 +65,51 @@ def comment(text): return style(text, fg='green') @@ -632,7 +637,7 @@ index fde5816..5827a55 100644 def format_requirement(ireq, marker=None): -@@ -63,10 +115,10 @@ def format_requirement(ireq, marker=None): +@@ -63,10 +120,10 @@ def format_requirement(ireq, marker=None): if ireq.editable: line = '-e {}'.format(ireq.link) else: diff --git a/tests/integration/test_lock.py b/tests/integration/test_lock.py index 0dfd22bb..1ab34f1b 100644 --- a/tests/integration/test_lock.py +++ b/tests/integration/test_lock.py @@ -1,5 +1,6 @@ import pytest import os +import six from pipenv.utils import temp_environ @@ -331,7 +332,7 @@ requests = "==2.14.0" @pytest.mark.lock @pytest.mark.vcs @pytest.mark.needs_internet -def lock_editable_vcs_without_install(PipenvInstance, pypi): +def test_lock_editable_vcs_without_install(PipenvInstance, pypi): with PipenvInstance(pypi=pypi, chdir=True) as p: with open(p.pipfile_path, 'w') as f: f.write(""" @@ -345,3 +346,22 @@ requests = {git = "https://github.com/requests/requests.git", ref = "master", ed assert 'chardet' in p.lockfile['default'] c = p.pipenv('install') assert c.return_code == 0 + + +@pytest.mark.lock +@pytest.mark.skip(reason="This doesn't work for some reason.") +def test_lock_respecting_python_version(PipenvInstance, pypi): + with PipenvInstance(pypi=pypi, chdir=True) as p: + with open(p.pipfile_path, 'w') as f: + f.write(""" +[packages] +django = "*" + """.strip()) + c = p.pipenv('install ') + assert c.return_code == 0 + c = p.pipenv('run python --version') + assert c.return_code == 0 + py_version = c.err.splitlines()[-1].strip().split()[-1] + django_version = '==2.0.6' if py_version.startswith('3') else '==1.11.13' + assert py_version == '2.7.14' + assert p.lockfile['default']['django']['version'] == django_version diff --git a/tests/pypi/django/Django-1.11.13-py2.py3-none-any.whl b/tests/pypi/django/Django-1.11.13-py2.py3-none-any.whl new file mode 100644 index 00000000..38dfe813 Binary files /dev/null and b/tests/pypi/django/Django-1.11.13-py2.py3-none-any.whl differ diff --git a/tests/pypi/django/Django-1.11.13.tar.gz b/tests/pypi/django/Django-1.11.13.tar.gz new file mode 100644 index 00000000..ba99816e Binary files /dev/null and b/tests/pypi/django/Django-1.11.13.tar.gz differ diff --git a/tests/pypi/django/Django-2.0.6-py3-none-any.whl b/tests/pypi/django/Django-2.0.6-py3-none-any.whl new file mode 100644 index 00000000..b966b5d4 Binary files /dev/null and b/tests/pypi/django/Django-2.0.6-py3-none-any.whl differ diff --git a/tests/pypi/django/Django-2.0.6.tar.gz b/tests/pypi/django/Django-2.0.6.tar.gz new file mode 100644 index 00000000..bc69557b Binary files /dev/null and b/tests/pypi/django/Django-2.0.6.tar.gz differ