Skip to content

Commit f3a3c56

Browse files
committed
Patch piptools to use current environment python
- Fixes #2088, #2234, #1901 - Fully leverage piptools' compile functionality by using constraints in the same `RequirementSet` during resolution - Use `PIP_PYTHON_PATH` for compatibility check to filter out `requires_python` markers - Fix vcs resolution - Update JSON API endpoints - Enhance resolution for editable dependencies - Minor fix for adding packages to pipfiles Signed-off-by: Dan Ryan <[email protected]>
1 parent cecf6f8 commit f3a3c56

File tree

4 files changed

+106
-67
lines changed

4 files changed

+106
-67
lines changed

pipenv/patched/piptools/repositories/pypi.py

+26-10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import hashlib
66
import os
7+
import sys
78
from contextlib import contextmanager
89
from shutil import rmtree
910

@@ -20,14 +21,17 @@
2021
SafeFileCache,
2122
)
2223

23-
from notpip._vendor.packaging.requirements import InvalidRequirement
24+
from notpip._vendor.packaging.requirements import InvalidRequirement, Requirement
25+
from notpip._vendor.packaging.version import Version, InvalidVersion, parse as parse_version
26+
from notpip._vendor.packaging.specifiers import SpecifierSet
2427
from notpip._vendor.pyparsing import ParseException
2528

2629
from ..cache import CACHE_DIR
2730
from pipenv.environments import PIPENV_CACHE_DIR
2831
from ..exceptions import NoCandidateFound
29-
from ..utils import (fs_str, is_pinned_requirement, lookup_table,
30-
make_install_requirement)
32+
from ..utils import (fs_str, is_pinned_requirement, lookup_table, as_tuple, key_from_req,
33+
make_install_requirement, format_requirement, dedup)
34+
3135
from .base import BaseRepository
3236

3337

@@ -159,7 +163,15 @@ def find_best_match(self, ireq, prereleases=None):
159163
if ireq.editable:
160164
return ireq # return itself as the best match
161165

162-
all_candidates = self.find_all_candidates(ireq.name)
166+
_all_candidates = self.find_all_candidates(ireq.name)
167+
all_candidates = []
168+
py_version = parse_version(os.environ.get('PIP_PYTHON_VERSION', str(sys.version_info[:3])))
169+
for c in _all_candidates:
170+
if c.requires_python:
171+
python_specifier = SpecifierSet(c.requires_python)
172+
if not python_specifier.contains(py_version):
173+
continue
174+
all_candidates.append(c)
163175
candidates_by_version = lookup_table(all_candidates, key=lambda c: c.version, unique=True)
164176
try:
165177
matching_versions = ireq.specifier.filter((candidate.version for candidate in all_candidates),
@@ -194,11 +206,12 @@ def gen(ireq):
194206
r = self.session.get(url)
195207

196208
# TODO: Latest isn't always latest.
197-
latest = list(r.json()['releases'].keys())[-1]
198-
if str(ireq.req.specifier) == '=={0}'.format(latest):
199-
latest_url = 'https://pypi.org/pypi/{0}/{1}/json'.format(ireq.req.name, latest)
200-
latest_requires = self.session.get(latest_url)
201-
for requires in latest_requires.json().get('info', {}).get('requires_dist', {}):
209+
releases = list(r.json()['releases'].keys())
210+
match = [r for r in releases if '=={0}'.format(r) == str(ireq.req.specifier)]
211+
if match:
212+
release_url = 'https://pypi.org/pypi/{0}/{1}/json'.format(ireq.req.name, match[0])
213+
release_requires = self.session.get(release_url)
214+
for requires in release_requires.json().get('info', {}).get('requires_dist', {}):
202215
i = InstallRequirement.from_line(requires)
203216

204217
if 'extra' not in repr(i.markers):
@@ -245,7 +258,10 @@ def get_legacy_dependencies(self, ireq):
245258
setup_requires = self.finder.get_extras_links(
246259
dist.get_metadata_lines('requires.txt')
247260
)
248-
except TypeError:
261+
ireq.version = dist.version
262+
ireq.project_name = dist.project_name
263+
ireq.req = dist.as_requirement()
264+
except (TypeError, ValueError):
249265
pass
250266

251267
if ireq not in self._dependencies_cache:

pipenv/project.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from pathlib2 import Path
2121

2222
from .cmdparse import Script
23-
from .vendor.requirementslib import Requirement
23+
from .vendor.requirementslib.requirements import Requirement
2424
from .utils import (
2525
atomic_open_for_write,
2626
mkdir_p,
@@ -728,8 +728,8 @@ def add_package_to_pipfile(self, package_name, dev=False):
728728
# Read and append Pipfile.
729729
p = self.parsed_pipfile
730730
# Don't re-capitalize file URLs or VCSs.
731-
package = Requirement.from_line(package_name)
732-
converted = first(package.as_pipfile().values())
731+
package = Requirement.from_line(package_name.strip())
732+
_, converted = package.pipfile_entry
733733
key = 'dev-packages' if dev else 'packages'
734734
# Set empty group if it doesn't exist yet.
735735
if key not in p:

pipenv/utils.py

+24-32
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ def actually_resolve_reps(
221221
):
222222
from .patched.notpip._internal import basecommand
223223
from .patched.notpip._internal.req import parse_requirements
224+
from .patched.notpip._internal.req.req_install import InstallRequirement
224225
from .patched.notpip._vendor import requests as pip_requests
225226
from .patched.notpip._internal.exceptions import DistributionNotFound
226227
from .patched.notpip._vendor.requests.exceptions import HTTPError
@@ -236,48 +237,37 @@ class PipCommand(basecommand.Command):
236237
name = 'PipCommand'
237238

238239
constraints = []
239-
tmpfile_constraints = []
240240
cleanup_req_dir = False
241241
if not req_dir:
242242
req_dir = TemporaryDirectory(suffix='-requirements', prefix='pipenv-')
243243
cleanup_req_dir = True
244244
for dep in deps:
245245
if dep:
246-
if dep.startswith('-e '):
247-
constraint = req.InstallRequirement.from_editable(
248-
dep[len('-e '):]
249-
)
250-
else:
251-
tmpfile_constraints.append(dep)
252-
req = Requirement.from_line(dep)
253-
# extra_constraints = []
246+
url = None
254247
if ' -i ' in dep:
255-
index_lookup[req.name] = project.get_source(
256-
url=dep.split(' -i ')[1]
257-
).get(
258-
'name'
259-
)
260-
if dep.markers:
261-
markers_lookup[dep.name] = str(
262-
dep.markers_as_pip
263-
).replace(
264-
'"', "'"
265-
)
266-
constraints.append(req)
248+
dep, url = dep.split(' -i ')
249+
req = Requirement.from_line(dep)
250+
_line = req.as_line()
251+
constraints.append(_line)
252+
# extra_constraints = []
253+
if url:
254+
index_lookup[req.name] = project.get_source(url=url).get('name')
255+
if req.markers:
256+
markers_lookup[req.name] = req.markers_as_pip
267257
constraints_file = None
268-
with NamedTemporaryFile(mode='w', prefix='pipenv-', suffix='-constraints.txt', dir=req_dir.name, delete=False) as f:
269-
f.write('\n'.join(tmpfile_constraints))
270-
constraints_file = f.name
271258
pip_command = get_pip_command()
272259
pip_args = []
273260
if sources:
274261
pip_args = prepare_pip_source_args(sources, pip_args)
262+
with NamedTemporaryFile(mode='w', prefix='pipenv-', suffix='-constraints.txt', dir=req_dir.name, delete=False) as f:
263+
f.write(u'\n'.join([_constraint for _constraint in constraints]))
264+
constraints_file = f.name
275265
if verbose:
276266
print('Using pip: {0}'.format(' '.join(pip_args)))
277267
pip_options, _ = pip_command.parse_args(pip_args)
278268
session = pip_command._build_session(pip_options)
279269
pypi = PyPIRepository(
280-
pip_options=pip_options, use_json=False, session=session
270+
pip_options=pip_options, use_json=True, session=session
281271
)
282272
if verbose:
283273
logging.log.verbose = True
@@ -1138,15 +1128,15 @@ def install_or_update_vcs(vcs_obj, src_dir, name, rev=None):
11381128

11391129

11401130
def get_vcs_deps(project, pip_freeze=None, which=None, verbose=False, clear=False, pre=False, allow_global=False, dev=False):
1141-
from ._compat import vcs
1131+
from .patched.notpip._internal.vcs import VcsSupport
11421132
section = 'vcs_dev_packages' if dev else 'vcs_packages'
11431133
lines = []
11441134
lockfiles = []
11451135
try:
11461136
packages = getattr(project, section)
11471137
except AttributeError:
11481138
return [], []
1149-
vcs_registry = vcs()
1139+
vcs_registry = VcsSupport
11501140
vcs_uri_map = {
11511141
extract_uri_from_vcs_dep(v): {'name': k, 'ref': v.get('ref')}
11521142
for k, v in packages.items()
@@ -1162,13 +1152,15 @@ def get_vcs_deps(project, pip_freeze=None, which=None, verbose=False, clear=Fals
11621152
pipfile_rev = vcs_uri_map[_vcs_match]['ref']
11631153
src_dir = os.environ.get('PIP_SRC', os.path.join(project.virtualenv_location, 'src'))
11641154
mkdir_p(src_dir)
1155+
pipfile_req = Requirement.from_pipfile(pipfile_name, [], packages[pipfile_name])
11651156
names = {pipfile_name}
1166-
_pip_uri = line.lstrip('-e ')
1167-
backend_name = str(_pip_uri.split('+', 1)[0])
1168-
backend = vcs_registry._registry[first(b for b in vcs_registry if b == backend_name)]
1169-
__vcs = backend(url=_pip_uri)
1170-
1157+
backend = vcs_registry()._registry.get(pipfile_req.vcs)
1158+
# TODO: Why doesn't pip freeze list 'git+git://' formatted urls?
1159+
if line.startswith('-e ') and not '{0}+'.format(pipfile_req.vcs) in line:
1160+
line = line.replace('-e ', '-e {0}+'.format(pipfile_req.vcs))
11711161
installed = Requirement.from_line(line)
1162+
__vcs = backend(url=installed.req.uri)
1163+
11721164
names.add(installed.normalized_name)
11731165
locked_rev = None
11741166
for _name in names:

tasks/vendoring/patches/patched/piptools.patch

+53-22
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,18 @@ index 4e6174c..75f9b49 100644
1919
# NOTE
2020
# We used to store the cache dir under ~/.pip-tools, which is not the
2121
diff --git a/pipenv/patched/piptools/repositories/pypi.py b/pipenv/patched/piptools/repositories/pypi.py
22-
index 1c4b943..8320e14 100644
22+
index 1c4b943..858d697 100644
2323
--- a/pipenv/patched/piptools/repositories/pypi.py
2424
+++ b/pipenv/patched/piptools/repositories/pypi.py
25-
@@ -15,10 +15,16 @@ from .._compat import (
25+
@@ -4,6 +4,7 @@ from __future__ import (absolute_import, division, print_function,
26+
27+
import hashlib
28+
import os
29+
+import sys
30+
from contextlib import contextmanager
31+
from shutil import rmtree
32+
33+
@@ -15,13 +16,22 @@ from .._compat import (
2634
Wheel,
2735
FAVORITE_HASH,
2836
TemporaryDirectory,
@@ -32,15 +40,23 @@ index 1c4b943..8320e14 100644
3240
+ SafeFileCache,
3341
)
3442

35-
+from pip._vendor.packaging.requirements import InvalidRequirement
43+
+from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
44+
+from pip._vendor.packaging.version import Version, InvalidVersion, parse as parse_version
45+
+from pip._vendor.packaging.specifiers import SpecifierSet
3646
+from pip._vendor.pyparsing import ParseException
3747
+
3848
from ..cache import CACHE_DIR
3949
+from pipenv.environments import PIPENV_CACHE_DIR
4050
from ..exceptions import NoCandidateFound
41-
from ..utils import (fs_str, is_pinned_requirement, lookup_table,
42-
make_install_requirement)
43-
@@ -37,6 +43,40 @@ except ImportError:
51+
-from ..utils import (fs_str, is_pinned_requirement, lookup_table,
52+
- make_install_requirement)
53+
+from ..utils import (fs_str, is_pinned_requirement, lookup_table, as_tuple, key_from_req,
54+
+ make_install_requirement, format_requirement, dedup)
55+
+
56+
from .base import BaseRepository
57+
58+
59+
@@ -37,6 +47,40 @@ except ImportError:
4460
from pip.wheel import WheelCache
4561

4662

@@ -81,7 +97,7 @@ index 1c4b943..8320e14 100644
8197
class PyPIRepository(BaseRepository):
8298
DEFAULT_INDEX_URL = PyPI.simple_url
8399

84-
@@ -46,10 +86,11 @@ class PyPIRepository(BaseRepository):
100+
@@ -46,10 +90,11 @@ class PyPIRepository(BaseRepository):
85101
config), but any other PyPI mirror can be used if index_urls is
86102
changed/configured on the Finder.
87103
"""
@@ -95,7 +111,7 @@ index 1c4b943..8320e14 100644
95111

96112
index_urls = [pip_options.index_url] + pip_options.extra_index_urls
97113
if pip_options.no_index:
98-
@@ -74,11 +115,15 @@ class PyPIRepository(BaseRepository):
114+
@@ -74,11 +119,15 @@ class PyPIRepository(BaseRepository):
99115
# of all secondary dependencies for the given requirement, so we
100116
# only have to go to disk once for each requirement
101117
self._dependencies_cache = {}
@@ -113,9 +129,20 @@ index 1c4b943..8320e14 100644
113129

114130
def freshen_build_caches(self):
115131
"""
116-
@@ -116,8 +161,11 @@ class PyPIRepository(BaseRepository):
117-
118-
all_candidates = self.find_all_candidates(ireq.name)
132+
@@ -114,10 +163,21 @@ class PyPIRepository(BaseRepository):
133+
if ireq.editable:
134+
return ireq # return itself as the best match
135+
136+
- all_candidates = self.find_all_candidates(ireq.name)
137+
+ _all_candidates = self.find_all_candidates(ireq.name)
138+
+ all_candidates = []
139+
+ py_version = parse_version(os.environ.get('PIP_PYTHON_VERSION', str(sys.version_info[:3])))
140+
+ for c in _all_candidates:
141+
+ if c.requires_python:
142+
+ python_specifier = SpecifierSet(c.requires_python)
143+
+ if not python_specifier.contains(py_version):
144+
+ continue
145+
+ all_candidates.append(c)
119146
candidates_by_version = lookup_table(all_candidates, key=lambda c: c.version, unique=True)
120147
- matching_versions = ireq.specifier.filter((candidate.version for candidate in all_candidates),
121148
+ try:
@@ -126,7 +153,7 @@ index 1c4b943..8320e14 100644
126153

127154
# Reuses pip's internal candidate sort key to sort
128155
matching_candidates = [candidates_by_version[ver] for ver in matching_versions]
129-
@@ -126,11 +174,60 @@ class PyPIRepository(BaseRepository):
156+
@@ -126,11 +186,61 @@ class PyPIRepository(BaseRepository):
130157
best_candidate = max(matching_candidates, key=self.finder._candidate_sort_key)
131158

132159
# Turn the candidate into a pinned InstallRequirement
@@ -153,11 +180,12 @@ index 1c4b943..8320e14 100644
153180
+ r = self.session.get(url)
154181
+
155182
+ # TODO: Latest isn't always latest.
156-
+ latest = list(r.json()['releases'].keys())[-1]
157-
+ if str(ireq.req.specifier) == '=={0}'.format(latest):
158-
+ latest_url = 'https://pypi.org/pypi/{0}/{1}/json'.format(ireq.req.name, latest)
159-
+ latest_requires = self.session.get(latest_url)
160-
+ for requires in latest_requires.json().get('info', {}).get('requires_dist', {}):
183+
+ releases = list(r.json()['releases'].keys())
184+
+ match = [r for r in releases if '=={0}'.format(r) == str(ireq.req.specifier)]
185+
+ if match:
186+
+ release_url = 'https://pypi.org/pypi/{0}/{1}/json'.format(ireq.req.name, match[0])
187+
+ release_requires = self.session.get(release_url)
188+
+ for requires in release_requires.json().get('info', {}).get('requires_dist', {}):
161189
+ i = InstallRequirement.from_line(requires)
162190
+
163191
+ if 'extra' not in repr(i.markers):
@@ -190,7 +218,7 @@ index 1c4b943..8320e14 100644
190218
"""
191219
Given a pinned or an editable InstallRequirement, returns a set of
192220
dependencies (also InstallRequirements, but not necessarily pinned).
193-
@@ -139,6 +236,18 @@ class PyPIRepository(BaseRepository):
221+
@@ -139,6 +249,21 @@ class PyPIRepository(BaseRepository):
194222
if not (ireq.editable or is_pinned_requirement(ireq)):
195223
raise TypeError('Expected pinned or editable InstallRequirement, got {}'.format(ireq))
196224

@@ -203,13 +231,16 @@ index 1c4b943..8320e14 100644
203231
+ setup_requires = self.finder.get_extras_links(
204232
+ dist.get_metadata_lines('requires.txt')
205233
+ )
206-
+ except TypeError:
234+
+ ireq.version = dist.version
235+
+ ireq.project_name = dist.project_name
236+
+ ireq.req = dist.as_requirement()
237+
+ except (TypeError, ValueError):
207238
+ pass
208239
+
209240
if ireq not in self._dependencies_cache:
210241
if ireq.editable and (ireq.source_dir and os.path.exists(ireq.source_dir)):
211242
# No download_dir for locally available editable requirements.
212-
@@ -164,11 +273,14 @@ class PyPIRepository(BaseRepository):
243+
@@ -164,11 +289,14 @@ class PyPIRepository(BaseRepository):
213244
download_dir=download_dir,
214245
wheel_download_dir=self._wheel_download_dir,
215246
session=self.session,
@@ -226,7 +257,7 @@ index 1c4b943..8320e14 100644
226257
)
227258
except TypeError:
228259
# Pip >= 10 (new resolver!)
229-
@@ -190,14 +302,44 @@ class PyPIRepository(BaseRepository):
260+
@@ -190,14 +318,44 @@ class PyPIRepository(BaseRepository):
230261
upgrade_strategy="to-satisfy-only",
231262
force_reinstall=False,
232263
ignore_dependencies=False,
@@ -273,7 +304,7 @@ index 1c4b943..8320e14 100644
273304
reqset.cleanup_files()
274305
return set(self._dependencies_cache[ireq])
275306

276-
@@ -224,17 +366,10 @@ class PyPIRepository(BaseRepository):
307+
@@ -224,17 +382,10 @@ class PyPIRepository(BaseRepository):
277308
matching_candidates = candidates_by_version[matching_versions[0]]
278309

279310
return {

0 commit comments

Comments
 (0)