Merge pull request #6638 from cjerdonek/debug-command

Add a "pip debug" command
This commit is contained in:
Chris Jerdonek 2019-07-02 02:00:02 -07:00 committed by GitHub
commit a8510bc5e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 251 additions and 37 deletions

2
news/6638.feature Normal file
View File

@ -0,0 +1,2 @@
Add a new command ``pip debug`` that can display e.g. the list of compatible
tags for the current Python.

View File

@ -324,10 +324,7 @@ class RequirementCommand(Command):
self,
options, # type: Values
session, # type: PipSession
platform=None, # type: Optional[str]
py_version_info=None, # type: Optional[Tuple[int, ...]]
abi=None, # type: Optional[str]
implementation=None, # type: Optional[str]
target_python=None, # type: Optional[TargetPython]
ignore_requires_python=None, # type: Optional[bool]
):
# type: (...) -> PackageFinder
@ -346,13 +343,6 @@ class RequirementCommand(Command):
ignore_requires_python=ignore_requires_python,
)
target_python = TargetPython(
platform=platform,
py_version_info=py_version_info,
abi=abi,
implementation=implementation,
)
return PackageFinder.create(
search_scope=search_scope,
selection_prefs=selection_prefs,

View File

@ -22,6 +22,7 @@ from pip._internal.locations import USER_CACHE_DIR, src_prefix
from pip._internal.models.format_control import FormatControl
from pip._internal.models.index import PyPI
from pip._internal.models.search_scope import SearchScope
from pip._internal.models.target_python import TargetPython
from pip._internal.utils.hashes import STRONG_HASHES
from pip._internal.utils.misc import redact_password_from_url
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
@ -356,6 +357,7 @@ def find_links():
def make_search_scope(options, suppress_no_index=False):
# type: (Values, bool) -> SearchScope
"""
:param suppress_no_index: Whether to ignore the --no-index option
when constructing the SearchScope object.
@ -600,6 +602,26 @@ abi = partial(
) # type: Callable[..., Option]
def add_target_python_options(cmd_opts):
# type: (OptionGroup) -> None
cmd_opts.add_option(platform())
cmd_opts.add_option(python_version())
cmd_opts.add_option(implementation())
cmd_opts.add_option(abi())
def make_target_python(options):
# type: (Values) -> TargetPython
target_python = TargetPython(
platform=options.platform,
py_version_info=options.python_version,
abi=options.abi,
implementation=options.implementation,
)
return target_python
def prefer_binary():
# type: () -> Option
return Option(

View File

@ -4,7 +4,6 @@
import os
import sys
from pip import __version__
from pip._internal.cli import cmdoptions
from pip._internal.cli.parser import (
ConfigOptionParser, UpdatingDefaultsHelpFormatter,
@ -13,7 +12,7 @@ from pip._internal.commands import (
commands_dict, get_similar_commands, get_summaries,
)
from pip._internal.exceptions import CommandError
from pip._internal.utils.misc import get_prog
from pip._internal.utils.misc import get_pip_version, get_prog
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
@ -39,12 +38,7 @@ def create_main_parser():
parser = ConfigOptionParser(**parser_kw)
parser.disable_interspersed_args()
pip_pkg_dir = os.path.abspath(os.path.join(
os.path.dirname(__file__), "..", "..",
))
parser.version = 'pip %s from %s (python %s)' % (
__version__, pip_pkg_dir, sys.version[:3],
)
parser.version = get_pip_version()
# add the general options
gen_opts = cmdoptions.make_option_group(cmdoptions.general_group, parser)

View File

@ -5,6 +5,7 @@ from __future__ import absolute_import
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 @@ commands_order = [
WheelCommand,
HashCommand,
CompletionCommand,
DebugCommand,
HelpCommand,
] # type: List[Type[Command]]

View File

@ -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 <options>"""
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

View File

@ -5,6 +5,7 @@ import os
from pip._internal.cli import cmdoptions
from pip._internal.cli.base_command import RequirementCommand
from pip._internal.cli.cmdoptions import make_target_python
from pip._internal.legacy_resolve import Resolver
from pip._internal.operations.prepare import RequirementPreparer
from pip._internal.req import RequirementSet
@ -69,10 +70,7 @@ class DownloadCommand(RequirementCommand):
help=("Download packages into <dir>."),
)
cmd_opts.add_option(cmdoptions.platform())
cmd_opts.add_option(cmdoptions.python_version())
cmd_opts.add_option(cmdoptions.implementation())
cmd_opts.add_option(cmdoptions.abi())
cmdoptions.add_target_python_options(cmd_opts)
index_opts = cmdoptions.make_option_group(
cmdoptions.index_group,
@ -96,13 +94,11 @@ class DownloadCommand(RequirementCommand):
ensure_dir(options.download_dir)
with self._build_session(options) as session:
target_python = make_target_python(options)
finder = self._build_package_finder(
options=options,
session=session,
platform=options.platform,
py_version_info=options.python_version,
abi=options.abi,
implementation=options.implementation,
target_python=target_python,
)
build_delete = (not (options.no_clean or options.build_dir))
if options.cache_dir and not check_path_owner(options.cache_dir):

View File

@ -12,6 +12,7 @@ from pip._vendor import pkg_resources
from pip._internal.cache import WheelCache
from pip._internal.cli import cmdoptions
from pip._internal.cli.base_command import RequirementCommand
from pip._internal.cli.cmdoptions import make_target_python
from pip._internal.cli.status_codes import ERROR
from pip._internal.exceptions import (
CommandError, InstallationError, PreviousBuildDirError,
@ -114,10 +115,7 @@ class InstallCommand(RequirementCommand):
'<dir>. Use --upgrade to replace existing packages in <dir> '
'with new versions.'
)
cmd_opts.add_option(cmdoptions.platform())
cmd_opts.add_option(cmdoptions.python_version())
cmd_opts.add_option(cmdoptions.implementation())
cmd_opts.add_option(cmdoptions.abi())
cmdoptions.add_target_python_options(cmd_opts)
cmd_opts.add_option(
'--user',
@ -285,13 +283,11 @@ class InstallCommand(RequirementCommand):
global_options = options.global_options or []
with self._build_session(options) as session:
target_python = make_target_python(options)
finder = self._build_package_finder(
options=options,
session=session,
platform=options.platform,
py_version_info=options.python_version,
abi=options.abi,
implementation=options.implementation,
target_python=target_python,
ignore_requires_python=options.ignore_requires_python,
)
build_delete = (not (options.no_clean or options.build_dir))

View File

@ -5,7 +5,8 @@ from pip._internal.utils.misc import normalize_version_info
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
from typing import Optional, Tuple
from typing import List, Optional, Tuple
from pip._internal.pep425tags import Pep425Tag
class TargetPython(object):
@ -54,9 +55,32 @@ class TargetPython(object):
self.py_version_info = py_version_info
# This is used to cache the return value of get_tags().
self._valid_tags = None
self._valid_tags = None # type: Optional[List[Pep425Tag]]
def format_given(self):
# type: () -> str
"""
Format the given, non-None attributes for display.
"""
display_version = None
if self._given_py_version_info is not None:
display_version = '.'.join(
str(part) for part in self._given_py_version_info
)
key_values = [
('platform', self.platform),
('version_info', display_version),
('abi', self.abi),
('implementation', self.implementation),
]
return ' '.join(
'{}={!r}'.format(key, value) for key, value in key_values
if value is not None
)
def get_tags(self):
# type: () -> List[Pep425Tag]
"""
Return the supported tags to check wheel candidates against.
"""

View File

@ -28,6 +28,7 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse
from pip._vendor.six.moves.urllib import request as urllib_request
from pip._vendor.six.moves.urllib.parse import unquote as urllib_unquote
from pip import __version__
from pip._internal.exceptions import CommandError, InstallationError
from pip._internal.locations import (
running_under_virtualenv, site_packages, user_site, virtualenv_no_global,
@ -104,6 +105,18 @@ except ImportError:
logger.debug('lzma module is not available')
def get_pip_version():
# type: () -> str
pip_pkg_dir = os.path.join(os.path.dirname(__file__), "..", "..")
pip_pkg_dir = os.path.abspath(pip_pkg_dir)
return (
'pip {} from {} (python {})'.format(
__version__, pip_pkg_dir, sys.version[:3],
)
)
def normalize_version_info(py_version_info):
# type: (Optional[Tuple[int, ...]]) -> Optional[Tuple[int, int, int]]
"""

View File

@ -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

View File

@ -47,6 +47,29 @@ class TestTargetPython:
assert target_python.py_version_info == CURRENT_PY_VERSION_INFO
assert target_python.py_version == current_major_minor
@pytest.mark.parametrize('kwargs, expected', [
({}, ''),
(dict(py_version_info=(3, 6)), "version_info='3.6'"),
(
dict(platform='darwin', py_version_info=(3, 6)),
"platform='darwin' version_info='3.6'",
),
(
dict(
platform='darwin', py_version_info=(3, 6), abi='cp36m',
implementation='cp'
),
(
"platform='darwin' version_info='3.6' abi='cp36m' "
"implementation='cp'"
),
),
])
def test_format_given(self, kwargs, expected):
target_python = TargetPython(**kwargs)
actual = target_python.format_given()
assert actual == expected
@pytest.mark.parametrize('py_version_info, expected_versions', [
((), ['']),
((2, ), ['2']),