Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix pip download VCS scp-like URLs #6296

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/6296.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix ``pip download`` scp-like VCS URLs. For example, ``pip download [email protected]:pypa/pip``.
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[isort]
skip =
.tox,
.venv,
.scratch,
_vendor,
data
Expand All @@ -16,6 +17,7 @@ include_trailing_comma = true
[flake8]
exclude =
.tox,
.venv,
.scratch,
_vendor,
data
Expand Down
30 changes: 19 additions & 11 deletions src/pip/_internal/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
)
from pip._internal.models.link import Link
from pip._internal.utils.hashes import Hashes
from pip._internal.vcs import AuthInfo
from pip._internal.vcs import AuthInfo, VersionControl

try:
import ssl # noqa
Expand Down Expand Up @@ -456,11 +456,15 @@ def get_file_content(url, comes_from=None, session=None):

def is_url(name):
# type: (Union[str, Text]) -> bool
"""Returns true if the name looks like a URL"""
"""Returns true if the name looks like a URL or a valid VCS URL"""
if is_vcs_url(name):
return True

if ':' not in name:
return False

scheme = name.split(':', 1)[0].lower()
return scheme in ['http', 'https', 'file', 'ftp'] + vcs.all_schemes
return scheme in ['http', 'https', 'file', 'ftp']


def url_to_path(url):
Expand Down Expand Up @@ -502,20 +506,24 @@ def is_archive_file(name):


def unpack_vcs_link(link, location):
vcs_backend = _get_used_vcs_backend(link)
# type: (Link, str) -> None
backend_cls = vcs.get_backend_by_url(link.url)
vcs_backend = backend_cls(link.url)
vcs_backend.unpack(location)


def _get_used_vcs_backend(link):
def _get_used_vcs_backend(url):
# type: (Text) -> Optional[VersionControl]
for backend in vcs.backends:
if link.scheme in backend.schemes:
vcs_backend = backend(link.url)
if backend.is_valid_url(url):
vcs_backend = backend(url)
return vcs_backend
return None


def is_vcs_url(link):
# type: (Link) -> bool
return bool(_get_used_vcs_backend(link))
def is_vcs_url(url):
# type: (Text) -> bool
return bool(vcs.get_backend_by_url(url))


def is_file_url(link):
Expand Down Expand Up @@ -857,7 +865,7 @@ def unpack_url(
would ordinarily raise HashUnsupported) are allowed.
"""
# non-editable vcs urls
if is_vcs_url(link):
if is_vcs_url(link.url):
unpack_vcs_link(link, location)

# file urls
Expand Down
6 changes: 1 addition & 5 deletions src/pip/_internal/models/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,4 @@ def is_artifact(self):
it points to an "abstract" thing like a path or a VCS location.
"""
from pip._internal.vcs import vcs

if self.scheme in vcs.all_schemes:
return False

return True
return not bool(vcs.get_backend_by_url(self.url))
5 changes: 2 additions & 3 deletions src/pip/_internal/operations/prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import display_path, normalize_path
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
from pip._internal.vcs import vcs

if MYPY_CHECK_RUNNING:
from typing import Any, Optional
Expand Down Expand Up @@ -288,7 +287,7 @@ def prepare_linked_requirement(
# we would report less-useful error messages for
# unhashable requirements, complaining that there's no
# hash provided.
if is_vcs_url(link):
if is_vcs_url(link.url):
raise VcsHashUnsupported()
elif is_file_url(link) and is_dir_url(link):
raise DirectoryUrlHashUnsupported()
Expand Down Expand Up @@ -349,7 +348,7 @@ def prepare_linked_requirement(
abstract_dist.prep_for_dist(finder, self.build_isolation)
if self._download_should_save:
# Make a .zip of the source_dir we already created.
if req.link.scheme in vcs.all_schemes:
if not req.link.is_artifact:
req.archive(self.download_dir)
return abstract_dist

Expand Down
23 changes: 23 additions & 0 deletions src/pip/_internal/vcs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,18 @@ def get_backend(self, name):
return self._registry[name]
return None

def get_backend_by_url(self, url):
# type: (Text) -> Optional[Type[VersionControl]]
"""
Return the backend class of the version control if found by given
URL, e.g. vcs.get_backend_by_url('git+https://github.com/pypa/pip.git')
"""

for backend in self.backends:
if backend.is_valid_url(url):
return backend
return None


vcs = VcsSupport()

Expand Down Expand Up @@ -532,3 +544,14 @@ def controls_location(cls, location):
the Git override checks that Git is actually available.
"""
return cls.is_repository_directory(location)

@classmethod
def is_valid_url(cls, url):
# type: (Text) -> bool
"""
Return whether an URL has a supported scheme for this Version Control.
"""
scheme = urllib_parse.urlsplit(url)[0]
if scheme in cls.schemes:
return True
return False
19 changes: 19 additions & 0 deletions src/pip/_internal/vcs/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@
display_path, make_vcs_requirement_url, redact_password_from_url,
)
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
from pip._internal.vcs import RemoteNotFoundError, VersionControl, vcs

if MYPY_CHECK_RUNNING:
from typing import Text

urlsplit = urllib_parse.urlsplit
urlunsplit = urllib_parse.urlunsplit

Expand Down Expand Up @@ -365,5 +369,20 @@ def controls_location(cls, location):
"because git is not available", location)
return False

@classmethod
def is_valid_url(cls, url):
# type: (Text) -> bool
"""
Return whether an URL has a supported scheme for this Version Control.

This overrided method handles scp-like URLs,
e.g. 'user@hostname:user/repo.git'.
"""

scheme, _, path, _, _ = urlsplit(url)
if not scheme and path.startswith('git+'):
return True
return super(Git, cls).is_valid_url(url)


vcs.register(Git)
16 changes: 16 additions & 0 deletions tests/functional/test_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,22 @@ def test_download_vcs_link(script):
assert script.site_packages / 'piptestpackage' not in result.files_created


@pytest.mark.network
def test_download_vcs_scp_like_url(script):
"""
It should download a vcs scp-like URL, for example:
git+user@hostname:user/repo.git. Test for the issue #6293.
"""
result = script.pip(
'download', '-d', '.', '[email protected]:pypa/pip-test-package.git'
)
assert (
Path('scratch') / 'pip-test-package-0.1.1.zip'
in result.files_created
)
assert script.site_packages / 'piptestpackage' not in result.files_created


def test_only_binary_set_then_download_specific_platform(script, data):
"""
Confirm that specifying an interpreter/platform constraint
Expand Down