Skip to content

Commit 5b93c09

Browse files
retpolanneVinicyus Macedo
authored and
Vinicyus Macedo
committed
Added test to fail pep508
1 parent 908e203 commit 5b93c09

File tree

4 files changed

+94
-21
lines changed

4 files changed

+94
-21
lines changed

news/6202.bugfix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix requirement line parser to correctly handle PEP 440 requirements with a URL
2+
pointing to an archive file.

src/pip/_internal/req/constructors.py

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
from pip._vendor.packaging.specifiers import Specifier
2121
from pip._vendor.pkg_resources import RequirementParseError, parse_requirements
2222

23-
from pip._internal.download import is_archive_file, is_url, url_to_path
23+
from pip._internal.download import (
24+
is_archive_file, is_url, path_to_url, url_to_path,
25+
)
2426
from pip._internal.exceptions import InstallationError
2527
from pip._internal.models.index import PyPI, TestPyPI
2628
from pip._internal.models.link import Link
@@ -201,6 +203,58 @@ def install_req_from_editable(
201203
)
202204

203205

206+
def _get_path_or_url(path, name):
207+
# type: (str, str) -> str
208+
"""
209+
First, it checks whether a provided path is an installable directory
210+
(e.g. it has a setup.py). If it is, returns the path.
211+
212+
If false, check if the path is an archive file (such as a .whl).
213+
The function checks if the path is a file. If false, if the path has
214+
an @, it will treat it as a PEP 440 URL requirement and return the path.
215+
"""
216+
if os.path.isdir(path) and _looks_like_path(name):
217+
if not is_installable_dir(path):
218+
raise InstallationError(
219+
"Directory %r is not installable. Neither 'setup.py' "
220+
"nor 'pyproject.toml' found." % name
221+
)
222+
return path_to_url(path)
223+
elif is_archive_file(path):
224+
if os.path.isfile(path):
225+
return path_to_url(path)
226+
else:
227+
urlreq_parts = name.split('@', 1)
228+
if len(urlreq_parts) < 2 or _looks_like_path(urlreq_parts[0]):
229+
logger.warning(
230+
'Requirement %r looks like a filename, but the '
231+
'file does not exist',
232+
name
233+
)
234+
return path_to_url(path)
235+
# If the path contains '@' and the part before it does not look
236+
# like a path, try to treat it as a PEP 440 URL req instead.
237+
238+
239+
def _looks_like_path(name):
240+
# type: (str) -> bool
241+
"""Checks whether the string "looks like" a path on the filesystem.
242+
243+
This does not check whether the target actually exists, only judge from the
244+
apperance. Returns true if any of the following is true:
245+
246+
* A path separator is found (either os.path.sep or os.path.altsep).
247+
* The string starts with "." (current directory).
248+
"""
249+
if os.path.sep in name:
250+
return True
251+
if os.path.altsep is not None and os.path.altsep in name:
252+
return True
253+
if name.startswith('.'):
254+
return True
255+
return False
256+
257+
204258
def install_req_from_line(
205259
name, # type: str
206260
comes_from=None, # type: Optional[Union[str, InstallRequirement]]
@@ -241,26 +295,9 @@ def install_req_from_line(
241295
link = Link(name)
242296
else:
243297
p, extras_as_string = _strip_extras(path)
244-
looks_like_dir = os.path.isdir(p) and (
245-
os.path.sep in name or
246-
(os.path.altsep is not None and os.path.altsep in name) or
247-
name.startswith('.')
248-
)
249-
if looks_like_dir:
250-
if not is_installable_dir(p):
251-
raise InstallationError(
252-
"Directory %r is not installable. Neither 'setup.py' "
253-
"nor 'pyproject.toml' found." % name
254-
)
255-
link = Link(path_to_url(p))
256-
elif is_archive_file(p):
257-
if not os.path.isfile(p):
258-
logger.warning(
259-
'Requirement %r looks like a filename, but the '
260-
'file does not exist',
261-
name
262-
)
263-
link = Link(path_to_url(p))
298+
path_or_url = _get_path_or_url(p, name)
299+
if path_or_url:
300+
link = Link(path_or_url)
264301

265302
# it's a local file, dir, or url
266303
if link:

tests/unit/test_req.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,33 @@ def test_url_with_query(self):
335335
req = install_req_from_line(url + fragment)
336336
assert req.link.url == url + fragment, req.link
337337

338+
def test_pep440_wheel_link_requirement(self):
339+
url = 'https://whatever.com/test-0.4-py2.py3-bogus-any.whl'
340+
line = 'test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl'
341+
req = install_req_from_line(line)
342+
parts = str(req.req).split('@', 1)
343+
assert len(parts) == 2
344+
assert parts[0].strip() == 'test'
345+
assert parts[1].strip() == url
346+
347+
def test_pep440_url_link_requirement(self):
348+
url = 'git+http://foo.com@ref#egg=foo'
349+
line = 'foo @ git+http://foo.com@ref#egg=foo'
350+
req = install_req_from_line(line)
351+
parts = str(req.req).split('@', 1)
352+
assert len(parts) == 2
353+
assert parts[0].strip() == 'foo'
354+
assert parts[1].strip() == url
355+
356+
def test_url_with_authentication_link_requirement(self):
357+
url = 'https://[email protected]/test-0.4-py2.py3-bogus-any.whl'
358+
line = 'https://[email protected]/test-0.4-py2.py3-bogus-any.whl'
359+
req = install_req_from_line(line)
360+
assert req.link is not None
361+
assert req.link.is_wheel
362+
assert req.link.scheme == "https"
363+
assert req.link.url == url
364+
338365
def test_unsupported_wheel_link_requirement_raises(self):
339366
reqset = RequirementSet()
340367
req = install_req_from_line(

tests/unit/test_req_file.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,13 @@ def test_yield_line_requirement(self):
225225
req = install_req_from_line(line, comes_from=comes_from)
226226
assert repr(list(process_line(line, filename, 1))[0]) == repr(req)
227227

228+
def test_yield_pep440_line_requirement(self):
229+
line = 'SomeProject @ https://url/SomeProject-py2-py3-none-any.whl'
230+
filename = 'filename'
231+
comes_from = '-r %s (line %s)' % (filename, 1)
232+
req = install_req_from_line(line, comes_from=comes_from)
233+
assert repr(list(process_line(line, filename, 1))[0]) == repr(req)
234+
228235
def test_yield_line_constraint(self):
229236
line = 'SomeProject'
230237
filename = 'filename'

0 commit comments

Comments
 (0)