Skip to content

Commit 16af35c

Browse files
retpolanneVinicyus Macedo
authored and
Vinicyus Macedo
committed
Adding improvements to the _get_path_to_url function
1 parent 5b93c09 commit 16af35c

File tree

3 files changed

+153
-45
lines changed

3 files changed

+153
-45
lines changed

docs/html/reference/pip_install.rst

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,7 @@ pip supports installing from a package index using a :term:`requirement
244244
specifier <pypug:Requirement Specifier>`. Generally speaking, a requirement
245245
specifier is composed of a project name followed by optional :term:`version
246246
specifiers <pypug:Version Specifier>`. :pep:`508` contains a full specification
247-
of the format of a requirement (pip does not support the ``url_req`` form
248-
of specifier at this time).
247+
of the format of a requirement.
249248

250249
Some examples:
251250

@@ -265,6 +264,13 @@ Since version 6.0, pip also supports specifiers containing `environment markers
265264
SomeProject ==5.4 ; python_version < '2.7'
266265
SomeProject; sys_platform == 'win32'
267266

267+
Since version 19.1, pip also supports `direct references
268+
<https://www.python.org/dev/peps/pep-0440/#direct-references>`__ like so:
269+
270+
::
271+
272+
SomeProject @ file:///somewhere/...
273+
268274
Environment markers are supported in the command line and in requirements files.
269275

270276
.. note::
@@ -880,6 +886,14 @@ Examples
880886
$ pip install http://my.package.repo/SomePackage-1.0.4.zip
881887

882888

889+
#. Install a particular source archive file following :pep:`440` direct references.
890+
891+
::
892+
893+
$ pip install SomeProject==1.0.4@http://my.package.repo//SomeProject-1.2.3-py33-none-any.whl
894+
$ pip install "SomeProject==1.0.4 @ http://my.package.repo//SomeProject-1.2.3-py33-none-any.whl"
895+
896+
883897
#. Install from alternative package repositories.
884898

885899
Install from a different index, and not `PyPI`_ ::

src/pip/_internal/req/constructors.py

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@
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 (
24-
is_archive_file, is_url, path_to_url, url_to_path,
25-
)
23+
from pip._internal.download import is_archive_file, is_url, url_to_path
2624
from pip._internal.exceptions import InstallationError
2725
from pip._internal.models.index import PyPI, TestPyPI
2826
from pip._internal.models.link import Link
@@ -203,58 +201,60 @@ def install_req_from_editable(
203201
)
204202

205203

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-
239204
def _looks_like_path(name):
240205
# type: (str) -> bool
241206
"""Checks whether the string "looks like" a path on the filesystem.
242207
243208
This does not check whether the target actually exists, only judge from the
244-
apperance. Returns true if any of the following is true:
209+
appearance.
245210
246-
* A path separator is found (either os.path.sep or os.path.altsep).
247-
* The string starts with "." (current directory).
211+
Returns true if any of the following conditions is true:
212+
* a path separator is found (either os.path.sep or os.path.altsep);
213+
* a dot is found (which represents the current directory).
248214
"""
249215
if os.path.sep in name:
250216
return True
251217
if os.path.altsep is not None and os.path.altsep in name:
252218
return True
253-
if name.startswith('.'):
219+
if name.startswith("."):
254220
return True
255221
return False
256222

257223

224+
def _get_url_from_path(path, name):
225+
# type: (str, str) -> str
226+
"""
227+
First, it checks whether a provided path is an installable directory
228+
(e.g. it has a setup.py). If it is, returns the path.
229+
230+
If false, check if the path is an archive file (such as a .whl).
231+
The function checks if the path is a file. If false, if the path has
232+
an @, it will treat it as a PEP 440 URL requirement and return the path.
233+
"""
234+
if _looks_like_path(name) and os.path.isdir(path):
235+
if is_installable_dir(path):
236+
return path_to_url(path)
237+
raise InstallationError(
238+
"Directory %r is not installable. Neither 'setup.py' "
239+
"nor 'pyproject.toml' found." % name
240+
)
241+
if not is_archive_file(path):
242+
return None
243+
if os.path.isfile(path):
244+
return path_to_url(path)
245+
urlreq_parts = name.split('@', 1)
246+
if len(urlreq_parts) >= 2 and not _looks_like_path(urlreq_parts[0]):
247+
# If the path contains '@' and the part before it does not look
248+
# like a path, try to treat it as a PEP 440 URL req instead.
249+
return None
250+
logger.warning(
251+
'Requirement %r looks like a filename, but the '
252+
'file does not exist',
253+
name
254+
)
255+
return path_to_url(path)
256+
257+
258258
def install_req_from_line(
259259
name, # type: str
260260
comes_from=None, # type: Optional[Union[str, InstallRequirement]]
@@ -295,9 +295,9 @@ def install_req_from_line(
295295
link = Link(name)
296296
else:
297297
p, extras_as_string = _strip_extras(path)
298-
path_or_url = _get_path_or_url(p, name)
299-
if path_or_url:
300-
link = Link(path_or_url)
298+
url = _get_url_from_path(p, name)
299+
if url is not None:
300+
link = Link(url)
301301

302302
# it's a local file, dir, or url
303303
if link:

tests/unit/test_req.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
from pip._internal.operations.prepare import RequirementPreparer
2222
from pip._internal.req import InstallRequirement, RequirementSet
2323
from pip._internal.req.constructors import (
24+
_get_url_from_path,
25+
_looks_like_path,
2426
install_req_from_editable,
2527
install_req_from_line,
2628
parse_editable,
@@ -653,3 +655,95 @@ def test_mismatched_versions(caplog, tmpdir):
653655
'Requested simplewheel==2.0, '
654656
'but installing version 1.0'
655657
)
658+
659+
660+
@pytest.mark.parametrize('args, expected', [
661+
# Test UNIX-like paths
662+
(('/path/to/installable'), True),
663+
# Test relative paths
664+
(('./path/to/installable'), True),
665+
# Test current path
666+
(('.'), True),
667+
# Test url paths
668+
(('https://whatever.com/test-0.4-py2.py3-bogus-any.whl'), True),
669+
# Test pep440 paths
670+
(('test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl'), True),
671+
# Test wheel
672+
(('simple-0.1-py2.py3-none-any.whl'), False),
673+
])
674+
def test_looks_like_path(args, expected):
675+
assert _looks_like_path(args) == expected
676+
677+
678+
@pytest.mark.skipif(
679+
not sys.platform.startswith("win"),
680+
reason='Test only available on Windows'
681+
)
682+
@pytest.mark.parametrize('args, expected', [
683+
# Test relative paths
684+
(('.\\path\\to\\installable'), True),
685+
(('relative\\path'), True),
686+
# Test absolute paths
687+
(('C:\\absolute\\path'), True),
688+
])
689+
def test_looks_like_path_win(args, expected):
690+
assert _looks_like_path(args) == expected
691+
692+
693+
@pytest.mark.parametrize('args, mock_returns, expected', [
694+
# Test pep440 urls
695+
(('/path/to/foo @ git+http://foo.com@ref#egg=foo',
696+
'foo @ git+http://foo.com@ref#egg=foo'), (False, False), None),
697+
# Test pep440 urls without spaces
698+
(('/path/to/foo@git+http://foo.com@ref#egg=foo',
699+
'foo @ git+http://foo.com@ref#egg=foo'), (False, False), None),
700+
# Test pep440 wheel
701+
(('/path/to/test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl',
702+
'test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl'),
703+
(False, False), None),
704+
# Test name is not a file
705+
(('/path/to/simple==0.1',
706+
'simple==0.1'),
707+
(False, False), None),
708+
])
709+
@patch('pip._internal.req.req_install.os.path.isdir')
710+
@patch('pip._internal.req.req_install.os.path.isfile')
711+
def test_get_url_from_path(
712+
isdir_mock, isfile_mock, args, mock_returns, expected
713+
):
714+
isdir_mock.return_value = mock_returns[0]
715+
isfile_mock.return_value = mock_returns[1]
716+
assert _get_url_from_path(*args) is expected
717+
718+
719+
@patch('pip._internal.req.req_install.os.path.isdir')
720+
@patch('pip._internal.req.req_install.os.path.isfile')
721+
def test_get_url_from_path__archive_file(isdir_mock, isfile_mock):
722+
isdir_mock.return_value = False
723+
isfile_mock.return_value = True
724+
name = 'simple-0.1-py2.py3-none-any.whl'
725+
path = os.path.join('/path/to/' + name)
726+
url = path_to_url(path)
727+
assert _get_url_from_path(path, name) == url
728+
729+
730+
@patch('pip._internal.req.req_install.os.path.isdir')
731+
@patch('pip._internal.req.req_install.os.path.isfile')
732+
def test_get_url_from_path__installable_dir(isdir_mock, isfile_mock):
733+
isdir_mock.return_value = True
734+
isfile_mock.return_value = True
735+
name = 'some/setuptools/project'
736+
path = os.path.join('/path/to/' + name)
737+
url = path_to_url(path)
738+
assert _get_url_from_path(path, name) == url
739+
740+
741+
@patch('pip._internal.req.req_install.os.path.isdir')
742+
def test_get_url_from_path__installable_error(isdir_mock):
743+
isdir_mock.return_value = True
744+
name = 'some/setuptools/project'
745+
path = os.path.join('/path/to/' + name)
746+
with pytest.raises(InstallationError) as e:
747+
_get_url_from_path(path, name)
748+
err_msg = e.value.args[0]
749+
assert "Neither 'setup.py' nor 'pyproject.toml' found" in err_msg

0 commit comments

Comments
 (0)