Skip to content

Commit de874f4

Browse files
committed
support URLs as packages
1 parent 263861d commit de874f4

File tree

13 files changed

+217
-116
lines changed

13 files changed

+217
-116
lines changed

piptools/_compat/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
Command,
1111
FormatControl,
1212
InstallRequirement,
13+
Link,
1314
PackageFinder,
1415
PyPI,
1516
RequirementSet,
@@ -20,6 +21,7 @@
2021
install_req_from_line,
2122
is_file_url,
2223
parse_requirements,
24+
path_to_url,
2325
stdlib_pkgs,
2426
url_to_path,
2527
user_cache_dir,

piptools/exceptions.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,6 @@ def __str__(self):
4848
return "\n".join(lines)
4949

5050

51-
class UnsupportedConstraint(PipToolsError):
52-
def __init__(self, message, constraint):
53-
super(UnsupportedConstraint, self).__init__(message)
54-
self.constraint = constraint
55-
56-
def __str__(self):
57-
message = super(UnsupportedConstraint, self).__str__()
58-
return "{} (constraint was: {})".format(message, str(self.constraint))
59-
60-
6151
class IncompatibleRequirements(PipToolsError):
6252
def __init__(self, ireq_a, ireq_b):
6353
self.ireq_a = ireq_a

piptools/repositories/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def find_best_match(self, ireq):
2525
@abstractmethod
2626
def get_dependencies(self, ireq):
2727
"""
28-
Given a pinned or an editable InstallRequirement, returns a set of
28+
Given a pinned, URL, or editable InstallRequirement, returns a set of
2929
dependencies (also InstallRequirements, but not necessarily pinned).
3030
They indicate the secondary dependencies for the given requirement.
3131
"""

piptools/repositories/pypi.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212

1313
from .._compat import (
1414
FAVORITE_HASH,
15+
Link,
1516
PackageFinder,
1617
PyPI,
1718
RequirementSet,
1819
TemporaryDirectory,
1920
Wheel,
2021
contextlib,
2122
is_file_url,
23+
path_to_url,
2224
url_to_path,
2325
)
2426
from ..cache import CACHE_DIR
@@ -137,7 +139,7 @@ def find_best_match(self, ireq, prereleases=None):
137139
Returns a Version object that indicates the best match for the given
138140
InstallRequirement according to the external repository.
139141
"""
140-
if ireq.editable or is_url_requirement(ireq, self):
142+
if ireq.editable or is_url_requirement(ireq):
141143
return ireq # return itself as the best match
142144

143145
all_candidates = self.find_all_candidates(ireq.name)
@@ -229,13 +231,17 @@ def resolve_reqs(self, download_dir, ireq, wheel_cache):
229231

230232
def get_dependencies(self, ireq):
231233
"""
232-
Given a pinned, a url, or an editable InstallRequirement, returns a set of
234+
Given a pinned, URL, or editable InstallRequirement, returns a set of
233235
dependencies (also InstallRequirements, but not necessarily pinned).
234236
They indicate the secondary dependencies for the given requirement.
235237
"""
236-
if not (ireq.editable or is_url_requirement(ireq, self) or is_pinned_requirement(ireq)):
238+
if not (
239+
ireq.editable or is_url_requirement(ireq) or is_pinned_requirement(ireq)
240+
):
237241
raise TypeError(
238-
"Expected url, pinned or editable InstallRequirement, got {}".format(ireq)
242+
"Expected url, pinned or editable InstallRequirement, got {}".format(
243+
ireq
244+
)
239245
)
240246

241247
if ireq not in self._dependencies_cache:
@@ -282,6 +288,16 @@ def get_hashes(self, ireq):
282288
if ireq.editable:
283289
return set()
284290

291+
if is_url_requirement(ireq):
292+
# url requirements may have been previously downloaded and cached
293+
# locally by self.resolve_reqs()
294+
cached_path = os.path.join(self._download_dir, ireq.link.filename)
295+
if os.path.exists(cached_path):
296+
cached_link = Link(path_to_url(cached_path))
297+
else:
298+
cached_link = ireq.link
299+
return {self._get_file_hash(cached_link)}
300+
285301
if not is_pinned_requirement(ireq):
286302
raise TypeError("Expected pinned requirement, got {}".format(ireq))
287303

piptools/resolver.py

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from . import click
1010
from ._compat import install_req_from_line
1111
from .cache import DependencyCache
12-
from .exceptions import UnsupportedConstraint
1312
from .logging import log
1413
from .utils import (
1514
UNSAFE_PACKAGES,
@@ -102,8 +101,6 @@ def resolve(self, max_rounds=10):
102101
self.dependency_cache.clear()
103102
self.repository.clear_caches()
104103

105-
self.check_constraints(chain(self.our_constraints, self.their_constraints))
106-
107104
# Ignore existing packages
108105
os.environ[str("PIP_EXISTS_ACTION")] = str(
109106
"i"
@@ -140,16 +137,6 @@ def resolve(self, max_rounds=10):
140137
# Only include hard requirements and not pip constraints
141138
return {req for req in best_matches if not req.constraint}
142139

143-
@staticmethod
144-
def check_constraints(constraints):
145-
for constraint in constraints:
146-
if constraint.link is not None and constraint.link.url.startswith('file:') and not constraint.editable:
147-
msg = (
148-
"pip-compile does not support file URLs as packages, unless "
149-
"they are editable. Perhaps add -e option?"
150-
)
151-
raise UnsupportedConstraint(msg, constraint)
152-
153140
def _group_constraints(self, constraints):
154141
"""
155142
Groups constraints (remember, InstallRequirements!) by their key name,
@@ -281,7 +268,7 @@ def get_best_match(self, ireq):
281268
Flask==0.10.1 => Flask==0.10.1
282269
283270
"""
284-
if ireq.editable or is_url_requirement(ireq, self.repository):
271+
if ireq.editable or is_url_requirement(ireq):
285272
# NOTE: it's much quicker to immediately return instead of
286273
# hitting the index server
287274
best_match = ireq
@@ -311,7 +298,7 @@ def _iter_dependencies(self, ireq):
311298
Editable requirements will never be looked up, as they may have
312299
changed at any time.
313300
"""
314-
if ireq.editable or is_url_requirement(ireq, self.repository):
301+
if ireq.editable or is_url_requirement(ireq):
315302
for dependency in self.repository.get_dependencies(ireq):
316303
yield dependency
317304
return
@@ -346,5 +333,7 @@ def _iter_dependencies(self, ireq):
346333
yield install_req_from_line(dependency_string, constraint=ireq.constraint)
347334

348335
def reverse_dependencies(self, ireqs):
349-
non_editable = [ireq for ireq in ireqs if not (ireq.editable or is_url_requirement(ireq, self.repository))]
336+
non_editable = [
337+
ireq for ireq in ireqs if not (ireq.editable or is_url_requirement(ireq))
338+
]
350339
return self.dependency_cache.reverse_dependencies(non_editable)

piptools/scripts/compile.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@
1414
from ..pip import get_pip_command, pip_defaults
1515
from ..repositories import LocalRequirementsRepository, PyPIRepository
1616
from ..resolver import Resolver
17-
from ..utils import UNSAFE_PACKAGES, dedup, is_pinned_requirement, key_from_req
17+
from ..utils import (
18+
UNSAFE_PACKAGES,
19+
dedup,
20+
is_pinned_requirement,
21+
key_from_ireq,
22+
key_from_req,
23+
)
1824
from ..writer import OutputWriter
1925

2026
DEFAULT_REQUIREMENTS_FILE = "requirements.in"
@@ -338,9 +344,6 @@ def cli(
338344
for find_link in repository.finder.find_links:
339345
log.debug(" -f {}".format(find_link))
340346

341-
# Check the given base set of constraints first
342-
Resolver.check_constraints(constraints)
343-
344347
try:
345348
resolver = Resolver(
346349
constraints,
@@ -413,10 +416,10 @@ def cli(
413416
unsafe_requirements=resolver.unsafe_constraints,
414417
reverse_dependencies=reverse_dependencies,
415418
primary_packages={
416-
key_from_req(ireq.req) for ireq in constraints if not ireq.constraint
419+
key_from_ireq(ireq) for ireq in constraints if not ireq.constraint
417420
},
418421
markers={
419-
key_from_req(ireq.req): ireq.markers for ireq in constraints if ireq.markers
422+
key_from_ireq(ireq): ireq.markers for ireq in constraints if ireq.markers
420423
},
421424
hashes=hashes,
422425
)

piptools/sync.py

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@
55
from subprocess import check_call # nosec
66

77
from . import click
8-
from .exceptions import IncompatibleRequirements, UnsupportedConstraint
8+
from ._compat import DEV_PKGS, stdlib_pkgs
9+
from .exceptions import IncompatibleRequirements
910
from .utils import (
1011
flat_map,
1112
format_requirement,
1213
get_hashes_from_ireq,
14+
is_url_requirement,
1315
key_from_ireq,
1416
key_from_req,
1517
)
1618

17-
from piptools._compat import DEV_PKGS, stdlib_pkgs
18-
1919
PACKAGES_TO_IGNORE = (
2020
["-markerlib", "pip", "pip-tools", "pip-review", "pkg-resources"]
2121
+ list(stdlib_pkgs)
@@ -77,14 +77,10 @@ def merge(requirements, ignore_conflicts):
7777
by_key = {}
7878

7979
for ireq in requirements:
80-
if ireq.link is not None and ireq.link.url.startswith('file:') and not ireq.editable:
81-
msg = (
82-
"pip-compile does not support file URLs as packages, unless they are "
83-
"editable. Perhaps add -e option?"
84-
)
85-
raise UnsupportedConstraint(msg, ireq)
86-
87-
key = ireq.link or key_from_req(ireq.req)
80+
# Limitation: URL requirements are merged by precise string match, so
81+
# "file:///example.zip#egg=example", "file:///example.zip", and
82+
# "example==1.0" will not merge with each other
83+
key = key_from_ireq(ireq)
8884

8985
if not ignore_conflicts:
9086
existing_ireq = by_key.get(key)
@@ -96,16 +92,35 @@ def merge(requirements, ignore_conflicts):
9692

9793
# TODO: Always pick the largest specifier in case of a conflict
9894
by_key[key] = ireq
99-
10095
return by_key.values()
10196

10297

98+
def diff_key_from_ireq(ireq):
99+
"""
100+
Calculate a key for comparing a compiled requirement with installed modules.
101+
For URL requirements, only provide a useful key if the url includes
102+
#egg=name==version, which will set ireq.req.name and ireq.specifier.
103+
Otherwise return ireq.link so the key will not match and the package will
104+
reinstall. Reinstall is necessary to ensure that packages will reinstall
105+
if the URL is changed but the version is not.
106+
"""
107+
if is_url_requirement(ireq):
108+
if (
109+
ireq.req
110+
and (getattr(ireq.req, "key", None) or getattr(ireq.req, "name", None))
111+
and ireq.specifier
112+
):
113+
return key_from_ireq(ireq)
114+
return str(ireq.link)
115+
return key_from_ireq(ireq)
116+
117+
103118
def diff(compiled_requirements, installed_dists):
104119
"""
105120
Calculate which packages should be installed or uninstalled, given a set
106121
of compiled requirements and a list of currently installed modules.
107122
"""
108-
requirements_lut = {r.link or key_from_req(r.req): r for r in compiled_requirements}
123+
requirements_lut = {diff_key_from_ireq(r): r for r in compiled_requirements}
109124

110125
satisfied = set() # holds keys
111126
to_install = set() # holds InstallRequirement objects

piptools/utils.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,12 @@ def make_install_requirement(name, version, extras, constraint=False):
5959
)
6060

6161

62-
def is_url_requirement(ireq, repository=None):
62+
def is_url_requirement(ireq):
6363
"""
64-
Finds if a requirement is a URL
64+
Return True if requirement was specified as a path or URL.
65+
ireq.original_link will have been set by InstallRequirement.__init__
6566
"""
66-
if not ireq.link or 'pypi.python.org' in ireq.link.url or ireq.link.url.startswith('file'):
67-
return False
68-
if repository is not None and hasattr(repository, 'finder'):
69-
if any(index_url in ireq.link.url for index_url in repository.finder.index_urls):
70-
return False
71-
return True
67+
return bool(ireq.original_link)
7268

7369

7470
def format_requirement(ireq, marker=None, hashes=None):
@@ -124,10 +120,7 @@ def is_pinned_requirement(ireq):
124120
if ireq.editable:
125121
return False
126122

127-
try:
128-
if len(ireq.specifier._specs) != 1:
129-
return False
130-
except Exception:
123+
if ireq.req is None or len(ireq.specifier._specs) != 1:
131124
return False
132125

133126
op, version = next(iter(ireq.specifier._specs))._spec

piptools/writer.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
dedup,
1212
format_requirement,
1313
get_compile_command,
14-
key_from_req,
14+
key_from_ireq,
1515
)
1616

1717

@@ -129,7 +129,7 @@ def _iter_lines(
129129
ireq,
130130
reverse_dependencies,
131131
primary_packages,
132-
markers.get(key_from_req(ireq.req)),
132+
markers.get(key_from_ireq(ireq)),
133133
hashes=hashes,
134134
)
135135
yield line
@@ -147,7 +147,7 @@ def _iter_lines(
147147
ireq,
148148
reverse_dependencies,
149149
primary_packages,
150-
marker=markers.get(key_from_req(ireq.req)),
150+
marker=markers.get(key_from_ireq(ireq)),
151151
hashes=hashes,
152152
)
153153
if not self.allow_unsafe:
@@ -185,7 +185,7 @@ def _format_requirement(
185185

186186
line = format_requirement(ireq, marker=marker, hashes=ireq_hashes)
187187

188-
if not self.annotate or key_from_req(ireq.req) in primary_packages:
188+
if not self.annotate or key_from_ireq(ireq) in primary_packages:
189189
return line
190190

191191
# Annotate what packages this package is required by

0 commit comments

Comments
 (0)