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 freeze not showing correct entry for mercurial packages that use subdirectories. #7072

Merged
merged 17 commits into from
Oct 12, 2019
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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/7071.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix ``pip freeze`` not showing correct entry for mercurial packages that use subdirectories.
13 changes: 8 additions & 5 deletions src/pip/_internal/utils/subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ def call_subprocess(
command_desc=None, # type: Optional[str]
extra_environ=None, # type: Optional[Mapping[str, Any]]
unset_environ=None, # type: Optional[Iterable[str]]
spinner=None # type: Optional[SpinnerInterface]
spinner=None, # type: Optional[SpinnerInterface]
log_failed_cmd=True # type: Optional[bool]
):
# type: (...) -> Text
"""
Expand All @@ -134,6 +135,7 @@ def call_subprocess(
acceptable, in addition to 0. Defaults to None, which means [].
unset_environ: an iterable of environment variable names to unset
prior to calling subprocess.Popen().
log_failed_cmd: if false, failed commands are not logged, only raised.
"""
if extra_ok_returncodes is None:
extra_ok_returncodes = []
Expand Down Expand Up @@ -189,9 +191,10 @@ def call_subprocess(
)
proc.stdin.close()
except Exception as exc:
subprocess_logger.critical(
"Error %s while executing command %s", exc, command_desc,
)
if log_failed_cmd:
chrahunt marked this conversation as resolved.
Show resolved Hide resolved
subprocess_logger.critical(
"Error %s while executing command %s", exc, command_desc,
)
raise
all_output = []
while True:
Expand Down Expand Up @@ -222,7 +225,7 @@ def call_subprocess(
spinner.finish("done")
if proc_had_error:
if on_returncode == 'raise':
if not showing_subprocess:
if not showing_subprocess and log_failed_cmd:
# Then the subprocess streams haven't been logged to the
# console yet.
msg = make_subprocess_output_error(
Expand Down
35 changes: 12 additions & 23 deletions src/pip/_internal/vcs/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
from pip._vendor.six.moves.urllib import request as urllib_request

from pip._internal.exceptions import BadCommand
from pip._internal.utils.compat import samefile
from pip._internal.utils.misc import display_path
from pip._internal.utils.subprocess import make_command
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
from pip._internal.vcs.versioncontrol import (
RemoteNotFoundError,
VersionControl,
find_path_to_setup_from_repo_root,
vcs,
)

Expand Down Expand Up @@ -295,30 +295,18 @@ def get_revision(cls, location, rev=None):

@classmethod
def get_subdirectory(cls, location):
"""
Return the path to setup.py, relative to the repo root.
Return None if setup.py is in the repo root.
"""
# find the repo root
git_dir = cls.run_command(['rev-parse', '--git-dir'],
show_stdout=False, cwd=location).strip()
git_dir = cls.run_command(
['rev-parse', '--git-dir'],
show_stdout=False, cwd=location).strip()
if not os.path.isabs(git_dir):
git_dir = os.path.join(location, git_dir)
root_dir = os.path.join(git_dir, '..')
# find setup.py
orig_location = location
while not os.path.exists(os.path.join(location, 'setup.py')):
last_location = location
location = os.path.dirname(location)
if location == last_location:
# We've traversed up to the root of the filesystem without
# finding setup.py
logger.warning(
"Could not find setup.py for directory %s (tried all "
"parent directories)",
orig_location,
)
return None
# relative path of setup.py to repo root
if samefile(root_dir, location):
return None
return os.path.relpath(location, root_dir)
repo_root = os.path.abspath(os.path.join(git_dir, '..'))
return find_path_to_setup_from_repo_root(location, repo_root)

@classmethod
def get_url_rev_and_auth(cls, url):
Expand Down Expand Up @@ -372,7 +360,8 @@ def controls_location(cls, location):
r = cls.run_command(['rev-parse'],
cwd=location,
show_stdout=False,
on_returncode='ignore')
on_returncode='ignore',
log_failed_cmd=False)
return not r
except BadCommand:
logger.debug("could not determine if %s is under git control "
Expand Down
34 changes: 33 additions & 1 deletion src/pip/_internal/vcs/mercurial.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@

from pip._vendor.six.moves import configparser

from pip._internal.exceptions import BadCommand, InstallationError
from pip._internal.utils.misc import display_path
from pip._internal.utils.subprocess import make_command
from pip._internal.utils.temp_dir import TempDirectory
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
from pip._internal.utils.urls import path_to_url
from pip._internal.vcs.versioncontrol import VersionControl, vcs
from pip._internal.vcs.versioncontrol import (
VersionControl,
find_path_to_setup_from_repo_root,
vcs,
)

if MYPY_CHECK_RUNNING:
from pip._internal.utils.misc import HiddenText
Expand Down Expand Up @@ -118,5 +123,32 @@ def is_commit_id_equal(cls, dest, name):
"""Always assume the versions don't match"""
return False

@classmethod
def get_subdirectory(cls, location):
"""
Return the path to setup.py, relative to the repo root.
Return None if setup.py is in the repo root.
"""
# find the repo root
repo_root = cls.run_command(
['root'], show_stdout=False, cwd=location).strip()
if not os.path.isabs(repo_root):
repo_root = os.path.abspath(os.path.join(location, repo_root))
return find_path_to_setup_from_repo_root(location, repo_root)

@classmethod
def controls_location(cls, location):
if super(Mercurial, cls).controls_location(location):
return True
try:
cls.run_command(
['identify'],
cwd=location,
show_stdout=False,
on_returncode='raise',
log_failed_cmd=False)
except (BadCommand, InstallationError):
return False


vcs.register(Mercurial)
54 changes: 46 additions & 8 deletions src/pip/_internal/vcs/versioncontrol.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from pip._vendor.six.moves.urllib import parse as urllib_parse

from pip._internal.exceptions import BadCommand
from pip._internal.utils.compat import samefile
from pip._internal.utils.misc import (
ask_path_exists,
backup_dir,
Expand Down Expand Up @@ -71,6 +72,33 @@ def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None):
return req


def find_path_to_setup_from_repo_root(location, repo_root):
"""
Find the path to `setup.py` by searching up the filesystem from `location`.
Return the path to `setup.py` relative to `repo_root`.
Return None if `setup.py` is in `repo_root` or cannot be found.
"""
# find setup.py
orig_location = location
while not os.path.exists(os.path.join(location, 'setup.py')):
last_location = location
location = os.path.dirname(location)
if location == last_location:
# We've traversed up to the root of the filesystem without
# finding setup.py
logger.warning(
"Could not find setup.py for directory %s (tried all "
"parent directories)",
orig_location,
)
return None

if samefile(repo_root, location):
return None

return os.path.relpath(location, repo_root)


class RemoteNotFoundError(Exception):
pass

Expand Down Expand Up @@ -240,9 +268,10 @@ def should_add_vcs_url_prefix(cls, remote_url):
return not remote_url.lower().startswith('{}:'.format(cls.name))

@classmethod
def get_subdirectory(cls, repo_dir):
def get_subdirectory(cls, location):
"""
Return the path to setup.py, relative to the repo root.
Return None if setup.py is in the repo root.
"""
return None

Expand Down Expand Up @@ -582,7 +611,8 @@ def run_command(
extra_ok_returncodes=None, # type: Optional[Iterable[int]]
command_desc=None, # type: Optional[str]
extra_environ=None, # type: Optional[Mapping[str, Any]]
spinner=None # type: Optional[SpinnerInterface]
spinner=None, # type: Optional[SpinnerInterface]
log_failed_cmd=True
):
# type: (...) -> Text
"""
Expand All @@ -598,7 +628,8 @@ def run_command(
command_desc=command_desc,
extra_environ=extra_environ,
unset_environ=cls.unset_environ,
spinner=spinner)
spinner=spinner,
log_failed_cmd=log_failed_cmd)
except OSError as e:
# errno.ENOENT = no such file or directory
# In other words, the VCS executable isn't available
Expand All @@ -625,10 +656,17 @@ def controls_location(cls, location):
# type: (str) -> bool
"""
Check if a location is controlled by the vcs.
It is meant to be overridden to implement smarter detection
mechanisms for specific vcs.

This can do more than is_repository_directory() alone. For example,
the Git override checks that Git is actually available.
Searches up the filesystem and checks is_repository_directory().

It is meant to be extended to add smarter detection mechanisms for
specific vcs. For example, the Git override checks that Git is
actually available.
"""
return cls.is_repository_directory(location)
while not cls.is_repository_directory(location):
chrahunt marked this conversation as resolved.
Show resolved Hide resolved
last_location = location
location = os.path.dirname(location)
if location == last_location:
# We've traversed up to the root of the filesystem.
return False
return True
40 changes: 40 additions & 0 deletions tests/functional/test_freeze.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,46 @@ def test_freeze_git_clone_srcdir(script, tmpdir):
_check_output(result.stdout, expected)


@need_mercurial
def test_freeze_mercurial_clone_srcdir(script, tmpdir):
"""
Test freezing a Mercurial clone where setup.py is in a subdirectory
relative to the repo root and the source code is in a subdirectory
relative to setup.py.
"""
# Returns path to a generated package called "version_pkg"
pkg_version = _create_test_package_with_srcdir(script, vcs='hg')

result = script.run(
'hg', 'clone', pkg_version, 'pip-test-package'
)
repo_dir = script.scratch_path / 'pip-test-package'
result = script.run(
'python', 'setup.py', 'develop',
cwd=repo_dir / 'subdir'
)
result = script.pip('freeze')
expected = textwrap.dedent(
"""
...-e hg+...#egg=version_pkg&subdirectory=subdir
...
"""
).strip()
_check_output(result.stdout, expected)

result = script.pip(
'freeze', '-f', '%s#egg=pip_test_package' % repo_dir
)
expected = textwrap.dedent(
"""
-f %(repo)s#egg=pip_test_package...
-e hg+...#egg=version_pkg&subdirectory=subdir
...
""" % {'repo': repo_dir},
).strip()
_check_output(result.stdout, expected)


@pytest.mark.git
def test_freeze_git_remote(script, tmpdir):
"""
Expand Down