mirror of https://github.com/pypa/pip
Implement new command 'pip index versions'
This commit is contained in:
parent
a90dd11e3f
commit
3751878b42
|
@ -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.
|
|
@ -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.',
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue