diff --git a/news/6638.feature b/news/6638.feature new file mode 100644 index 00000000000..f96b9133d29 --- /dev/null +++ b/news/6638.feature @@ -0,0 +1,2 @@ +Add a new command ``pip debug`` that can display e.g. the list of compatible +tags for the current Python. diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index 2e90db34f69..9e0ab86b9ca 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -5,6 +5,7 @@ from pip._internal.commands.completion import CompletionCommand from pip._internal.commands.configuration import ConfigurationCommand +from pip._internal.commands.debug import DebugCommand from pip._internal.commands.download import DownloadCommand from pip._internal.commands.freeze import FreezeCommand from pip._internal.commands.hash import HashCommand @@ -36,6 +37,7 @@ WheelCommand, HashCommand, CompletionCommand, + DebugCommand, HelpCommand, ] # type: List[Type[Command]] diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py new file mode 100644 index 00000000000..99b5b946f8a --- /dev/null +++ b/src/pip/_internal/commands/debug.py @@ -0,0 +1,102 @@ +from __future__ import absolute_import + +import logging +import sys + +from pip._internal.cli import cmdoptions +from pip._internal.cli.base_command import Command +from pip._internal.cli.cmdoptions import make_target_python +from pip._internal.cli.status_codes import SUCCESS +from pip._internal.utils.logging import indent_log +from pip._internal.utils.misc import get_pip_version +from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.wheel import format_tag + +if MYPY_CHECK_RUNNING: + from typing import Any, List + from optparse import Values + +logger = logging.getLogger(__name__) + + +def show_value(name, value): + # type: (str, str) -> None + logger.info('{}: {}'.format(name, value)) + + +def show_sys_implementation(): + # type: () -> None + logger.info('sys.implementation:') + if hasattr(sys, 'implementation'): + implementation = sys.implementation # type: ignore + implementation_name = implementation.name + else: + implementation_name = '' + + with indent_log(): + show_value('name', implementation_name) + + +def show_tags(options): + # type: (Values) -> None + tag_limit = 10 + + target_python = make_target_python(options) + tags = target_python.get_tags() + + # Display the target options that were explicitly provided. + formatted_target = target_python.format_given() + suffix = '' + if formatted_target: + suffix = ' (target: {})'.format(formatted_target) + + msg = 'Compatible tags: {}{}'.format(len(tags), suffix) + logger.info(msg) + + if options.verbose < 1 and len(tags) > tag_limit: + tags_limited = True + tags = tags[:tag_limit] + else: + tags_limited = False + + with indent_log(): + for tag in tags: + logger.info(format_tag(tag)) + + if tags_limited: + msg = ( + '...\n' + '[First {tag_limit} tags shown. Pass --verbose to show all.]' + ).format(tag_limit=tag_limit) + logger.info(msg) + + +class DebugCommand(Command): + """ + Display debug information. + """ + + name = 'debug' + usage = """ + %prog """ + summary = 'Show information useful for debugging.' + ignore_require_venv = True + + def __init__(self, *args, **kw): + super(DebugCommand, self).__init__(*args, **kw) + + cmd_opts = self.cmd_opts + cmdoptions.add_target_python_options(cmd_opts) + self.parser.insert_option_group(0, cmd_opts) + + def run(self, options, args): + # type: (Values, List[Any]) -> int + show_value('pip version', get_pip_version()) + show_value('sys.version', sys.version) + show_value('sys.executable', sys.executable) + show_value('sys.platform', sys.platform) + show_sys_implementation() + + show_tags(options) + + return SUCCESS diff --git a/tests/functional/test_debug.py b/tests/functional/test_debug.py new file mode 100644 index 00000000000..3f5374722da --- /dev/null +++ b/tests/functional/test_debug.py @@ -0,0 +1,50 @@ +import pytest + +from pip._internal import pep425tags + + +@pytest.mark.parametrize( + 'args', + [ + [], + ['--verbose'], + ] +) +def test_debug(script, args): + """ + Check simple option cases. + """ + args = ['debug'] + args + result = script.pip(*args) + stdout = result.stdout + + assert 'sys.executable: ' in stdout + assert 'sys.platform: ' in stdout + assert 'sys.implementation:' in stdout + + tags = pep425tags.get_supported() + expected_tag_header = 'Compatible tags: {}'.format(len(tags)) + assert expected_tag_header in stdout + + show_verbose_note = '--verbose' not in args + assert ( + '...\n [First 10 tags shown. Pass --verbose to show all.]' in stdout + ) == show_verbose_note + + +@pytest.mark.parametrize( + 'args, expected', + [ + (['--python-version', '3.7'], "(target: version_info='3.7')"), + ] +) +def test_debug__target_options(script, args, expected): + """ + Check passing target-related options. + """ + args = ['debug'] + args + result = script.pip(*args) + stdout = result.stdout + + assert 'Compatible tags: ' in stdout + assert expected in stdout