Implement new command 'pip index versions'

This commit is contained in:
Noah Gorny 2021-06-11 14:01:14 +03:00 committed by GitHub
parent a90dd11e3f
commit 3751878b42
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 247 additions and 15 deletions

2
news/7975.feature.rst Normal file
View File

@ -0,0 +1,2 @@
Add new subcommand ``pip index`` used to interact with indexes, and implement
``pip index version`` to list available versions of a package.

View File

@ -59,6 +59,10 @@ commands_dict = OrderedDict([
'pip._internal.commands.cache', 'CacheCommand',
"Inspect and manage pip's wheel cache.",
)),
('index', CommandInfo(
'pip._internal.commands.index', 'IndexCommand',
"Inspect information available from package indexes.",
)),
('wheel', CommandInfo(
'pip._internal.commands.wheel', 'WheelCommand',
'Build wheels from your requirements.',

View File

@ -0,0 +1,143 @@
import logging
from optparse import Values
from typing import Any, Iterable, List, Optional, Union
from pip._vendor.packaging.version import LegacyVersion, Version
from pip._internal.cli import cmdoptions
from pip._internal.cli.req_command import IndexGroupCommand
from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.commands.search import print_dist_installation_info
from pip._internal.exceptions import CommandError, DistributionNotFound, PipError
from pip._internal.index.collector import LinkCollector
from pip._internal.index.package_finder import PackageFinder
from pip._internal.models.selection_prefs import SelectionPreferences
from pip._internal.models.target_python import TargetPython
from pip._internal.network.session import PipSession
from pip._internal.utils.misc import write_output
logger = logging.getLogger(__name__)
class IndexCommand(IndexGroupCommand):
"""
Inspect information available from package indexes.
"""
usage = """
%prog versions <package>
"""
def add_options(self):
# type: () -> None
cmdoptions.add_target_python_options(self.cmd_opts)
self.cmd_opts.add_option(cmdoptions.ignore_requires_python())
self.cmd_opts.add_option(cmdoptions.pre())
self.cmd_opts.add_option(cmdoptions.no_binary())
self.cmd_opts.add_option(cmdoptions.only_binary())
index_opts = cmdoptions.make_option_group(
cmdoptions.index_group,
self.parser,
)
self.parser.insert_option_group(0, index_opts)
self.parser.insert_option_group(0, self.cmd_opts)
def run(self, options, args):
# type: (Values, List[Any]) -> int
handlers = {
"versions": self.get_available_package_versions,
}
logger.warning(
"pip index is currently an experimental command. "
"It may be removed/changed in a future release "
"without prior warning."
)
# Determine action
if not args or args[0] not in handlers:
logger.error(
"Need an action (%s) to perform.",
", ".join(sorted(handlers)),
)
return ERROR
action = args[0]
# Error handling happens here, not in the action-handlers.
try:
handlers[action](options, args[1:])
except PipError as e:
logger.error(e.args[0])
return ERROR
return SUCCESS
def _build_package_finder(
self,
options, # type: Values
session, # type: PipSession
target_python=None, # type: Optional[TargetPython]
ignore_requires_python=None, # type: Optional[bool]
):
# type: (...) -> PackageFinder
"""
Create a package finder appropriate to the index command.
"""
link_collector = LinkCollector.create(session, options=options)
# Pass allow_yanked=False to ignore yanked versions.
selection_prefs = SelectionPreferences(
allow_yanked=False,
allow_all_prereleases=options.pre,
ignore_requires_python=ignore_requires_python,
)
return PackageFinder.create(
link_collector=link_collector,
selection_prefs=selection_prefs,
target_python=target_python,
)
def get_available_package_versions(self, options, args):
# type: (Values, List[Any]) -> None
if len(args) != 1:
raise CommandError('You need to specify exactly one argument')
target_python = cmdoptions.make_target_python(options)
query = args[0]
with self._build_session(options) as session:
finder = self._build_package_finder(
options=options,
session=session,
target_python=target_python,
ignore_requires_python=options.ignore_requires_python,
)
versions: Iterable[Union[LegacyVersion, Version]] = (
candidate.version
for candidate in finder.find_all_candidates(query)
)
if not options.pre:
# Remove prereleases
versions = (version for version in versions
if not version.is_prerelease)
versions = set(versions)
if not versions:
raise DistributionNotFound(
'No matching distribution found for {}'.format(query))
formatted_versions = [str(ver) for ver in sorted(
versions, reverse=True)]
latest = formatted_versions[0]
write_output('{} ({})'.format(query, latest))
write_output('Available versions: {}'.format(
', '.join(formatted_versions)))
print_dist_installation_info(query, latest)

View File

@ -114,6 +114,23 @@ def transform_hits(hits):
return list(packages.values())
def print_dist_installation_info(name, latest):
# type: (str, str) -> None
env = get_default_environment()
dist = env.get_distribution(name)
if dist is not None:
with indent_log():
if dist.version == latest:
write_output('INSTALLED: %s (latest)', dist.version)
else:
write_output('INSTALLED: %s', dist.version)
if parse_version(latest).pre:
write_output('LATEST: %s (pre-release; install'
' with "pip install --pre")', latest)
else:
write_output('LATEST: %s', latest)
def print_results(hits, name_column_width=None, terminal_width=None):
# type: (List[TransformedHit], Optional[int], Optional[int]) -> None
if not hits:
@ -124,7 +141,6 @@ def print_results(hits, name_column_width=None, terminal_width=None):
for hit in hits
]) + 4
env = get_default_environment()
for hit in hits:
name = hit['name']
summary = hit['summary'] or ''
@ -141,18 +157,7 @@ def print_results(hits, name_column_width=None, terminal_width=None):
line = f'{name_latest:{name_column_width}} - {summary}'
try:
write_output(line)
dist = env.get_distribution(name)
if dist is not None:
with indent_log():
if dist.version == latest:
write_output('INSTALLED: %s (latest)', dist.version)
else:
write_output('INSTALLED: %s', dist.version)
if parse_version(latest).pre:
write_output('LATEST: %s (pre-release; install'
' with "pip install --pre")', latest)
else:
write_output('LATEST: %s', latest)
print_dist_installation_info(name, latest)
except UnicodeEncodeError:
pass

View File

@ -0,0 +1,75 @@
import pytest
from pip._internal.cli.status_codes import ERROR, SUCCESS
from pip._internal.commands import create_command
@pytest.mark.network
def test_list_all_versions_basic_search(script):
"""
End to end test of index versions command.
"""
output = script.pip('index', 'versions', 'pip', allow_stderr_warning=True)
assert 'Available versions:' in output.stdout
assert (
'20.2.3, 20.2.2, 20.2.1, 20.2, 20.1.1, 20.1, 20.0.2'
', 20.0.1, 19.3.1, 19.3, 19.2.3, 19.2.2, 19.2.1, 19.2, 19.1.1'
', 19.1, 19.0.3, 19.0.2, 19.0.1, 19.0, 18.1, 18.0, 10.0.1, 10.0.0, '
'9.0.3, 9.0.2, 9.0.1, 9.0.0, 8.1.2, 8.1.1, '
'8.1.0, 8.0.3, 8.0.2, 8.0.1, 8.0.0, 7.1.2, 7.1.1, 7.1.0, 7.0.3, '
'7.0.2, 7.0.1, 7.0.0, 6.1.1, 6.1.0, 6.0.8, 6.0.7, 6.0.6, 6.0.5, '
'6.0.4, 6.0.3, 6.0.2, 6.0.1, 6.0, 1.5.6, 1.5.5, 1.5.4, 1.5.3, '
'1.5.2, 1.5.1, 1.5, 1.4.1, 1.4, 1.3.1, 1.3, 1.2.1, 1.2, 1.1, 1.0.2,'
' 1.0.1, 1.0, 0.8.3, 0.8.2, 0.8.1, 0.8, 0.7.2, 0.7.1, 0.7, 0.6.3, '
'0.6.2, 0.6.1, 0.6, 0.5.1, 0.5, 0.4, 0.3.1, '
'0.3, 0.2.1, 0.2' in output.stdout
)
@pytest.mark.network
def test_list_all_versions_search_with_pre(script):
"""
See that adding the --pre flag adds pre-releases
"""
output = script.pip(
'index', 'versions', 'pip', '--pre', allow_stderr_warning=True)
assert 'Available versions:' in output.stdout
assert (
'20.2.3, 20.2.2, 20.2.1, 20.2, 20.2b1, 20.1.1, 20.1, 20.1b1, 20.0.2'
', 20.0.1, 19.3.1, 19.3, 19.2.3, 19.2.2, 19.2.1, 19.2, 19.1.1'
', 19.1, 19.0.3, 19.0.2, 19.0.1, 19.0, 18.1, 18.0, 10.0.1, 10.0.0, '
'10.0.0b2, 10.0.0b1, 9.0.3, 9.0.2, 9.0.1, 9.0.0, 8.1.2, 8.1.1, '
'8.1.0, 8.0.3, 8.0.2, 8.0.1, 8.0.0, 7.1.2, 7.1.1, 7.1.0, 7.0.3, '
'7.0.2, 7.0.1, 7.0.0, 6.1.1, 6.1.0, 6.0.8, 6.0.7, 6.0.6, 6.0.5, '
'6.0.4, 6.0.3, 6.0.2, 6.0.1, 6.0, 1.5.6, 1.5.5, 1.5.4, 1.5.3, '
'1.5.2, 1.5.1, 1.5, 1.4.1, 1.4, 1.3.1, 1.3, 1.2.1, 1.2, 1.1, 1.0.2,'
' 1.0.1, 1.0, 0.8.3, 0.8.2, 0.8.1, 0.8, 0.7.2, 0.7.1, 0.7, 0.6.3, '
'0.6.2, 0.6.1, 0.6, 0.5.1, 0.5, 0.4, 0.3.1, '
'0.3, 0.2.1, 0.2' in output.stdout
)
@pytest.mark.network
def test_list_all_versions_returns_no_matches_found_when_name_not_exact():
"""
Test that non exact name do not match
"""
command = create_command('index')
cmdline = "versions pand"
with command.main_context():
options, args = command.parse_args(cmdline.split())
status = command.run(options, args)
assert status == ERROR
@pytest.mark.network
def test_list_all_versions_returns_matches_found_when_name_is_exact():
"""
Test that exact name matches
"""
command = create_command('index')
cmdline = "versions pandas"
with command.main_context():
options, args = command.parse_args(cmdline.split())
status = command.run(options, args)
assert status == SUCCESS

View File

@ -11,7 +11,8 @@ from pip._internal.commands import commands_dict, create_command
# These are the expected names of the commands whose classes inherit from
# IndexGroupCommand.
EXPECTED_INDEX_GROUP_COMMANDS = ['download', 'install', 'list', 'wheel']
EXPECTED_INDEX_GROUP_COMMANDS = [
'download', 'index', 'install', 'list', 'wheel']
def check_commands(pred, expected):
@ -49,7 +50,9 @@ def test_session_commands():
def is_session_command(command):
return isinstance(command, SessionCommandMixin)
expected = ['download', 'install', 'list', 'search', 'uninstall', 'wheel']
expected = [
'download', 'index', 'install', 'list', 'search', 'uninstall', 'wheel'
]
check_commands(is_session_command, expected)