2014-08-31 01:52:28 +02:00
|
|
|
from __future__ import absolute_import
|
|
|
|
|
|
|
|
import logging
|
2010-02-22 23:03:05 +01:00
|
|
|
import sys
|
|
|
|
import textwrap
|
2017-03-18 18:14:49 +01:00
|
|
|
from collections import OrderedDict
|
|
|
|
|
2017-05-16 12:16:30 +02:00
|
|
|
from pip._vendor import pkg_resources
|
|
|
|
from pip._vendor.packaging.version import parse as parse_version
|
2020-09-23 16:27:09 +02:00
|
|
|
|
2017-07-17 19:15:17 +02:00
|
|
|
# NOTE: XMLRPC Client is not annotated in typeshed as on 2017-07-17, which is
|
|
|
|
# why we ignore the type on this import
|
|
|
|
from pip._vendor.six.moves import xmlrpc_client # type: ignore
|
2017-05-16 12:16:30 +02:00
|
|
|
|
2018-07-30 06:16:25 +02:00
|
|
|
from pip._internal.cli.base_command import Command
|
2019-08-07 22:04:02 +02:00
|
|
|
from pip._internal.cli.req_command import SessionCommandMixin
|
2018-07-30 06:16:25 +02:00
|
|
|
from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS
|
2017-08-31 17:48:18 +02:00
|
|
|
from pip._internal.exceptions import CommandError
|
2018-05-29 22:14:51 +02:00
|
|
|
from pip._internal.models.index import PyPI
|
2019-09-27 19:11:19 +02:00
|
|
|
from pip._internal.network.xmlrpc import PipXmlrpcTransport
|
2018-07-29 13:11:37 +02:00
|
|
|
from pip._internal.utils.compat import get_terminal_size
|
2017-08-31 17:48:18 +02:00
|
|
|
from pip._internal.utils.logging import indent_log
|
2020-05-14 20:44:15 +02:00
|
|
|
from pip._internal.utils.misc import get_distribution, write_output
|
2020-05-17 22:30:41 +02:00
|
|
|
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
|
|
|
|
|
|
|
|
if MYPY_CHECK_RUNNING:
|
|
|
|
from optparse import Values
|
2020-09-23 15:08:01 +02:00
|
|
|
from typing import Dict, List, Optional
|
|
|
|
|
2020-05-19 09:19:46 +02:00
|
|
|
from typing_extensions import TypedDict
|
|
|
|
TransformedHit = TypedDict(
|
|
|
|
'TransformedHit',
|
|
|
|
{'name': str, 'summary': str, 'versions': List[str]},
|
|
|
|
)
|
2011-08-02 19:16:18 +02:00
|
|
|
|
2014-08-31 01:52:28 +02:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2019-09-03 00:48:15 +02:00
|
|
|
class SearchCommand(Command, SessionCommandMixin):
|
2013-02-16 19:00:29 +01:00
|
|
|
"""Search for PyPI packages whose name or summary contains <query>."""
|
2019-07-10 09:36:33 +02:00
|
|
|
|
2013-01-18 22:25:15 +01:00
|
|
|
usage = """
|
|
|
|
%prog [options] <query>"""
|
2017-05-18 15:20:01 +02:00
|
|
|
ignore_require_venv = True
|
2010-02-22 23:03:05 +01:00
|
|
|
|
2020-05-22 11:31:58 +02:00
|
|
|
def add_options(self):
|
|
|
|
# type: () -> None
|
|
|
|
self.cmd_opts.add_option(
|
2016-03-20 23:11:29 +01:00
|
|
|
'-i', '--index',
|
2010-02-22 23:03:05 +01:00
|
|
|
dest='index',
|
|
|
|
metavar='URL',
|
2014-12-16 07:04:00 +01:00
|
|
|
default=PyPI.pypi_url,
|
2010-02-22 23:03:05 +01:00
|
|
|
help='Base URL of Python Package Index (default %default)')
|
|
|
|
|
2020-05-22 11:31:58 +02:00
|
|
|
self.parser.insert_option_group(0, self.cmd_opts)
|
2012-12-10 02:28:50 +01:00
|
|
|
|
2010-02-22 23:03:05 +01:00
|
|
|
def run(self, options, args):
|
2020-05-17 22:30:41 +02:00
|
|
|
# type: (Values, List[str]) -> int
|
2010-02-22 23:03:05 +01:00
|
|
|
if not args:
|
2011-07-20 21:04:11 +02:00
|
|
|
raise CommandError('Missing required argument (search query).')
|
2011-04-16 18:00:10 +02:00
|
|
|
query = args
|
2014-06-28 00:26:40 +02:00
|
|
|
pypi_hits = self.search(query, options)
|
2010-02-23 22:01:43 +01:00
|
|
|
hits = transform_hits(pypi_hits)
|
2010-02-22 23:03:05 +01:00
|
|
|
|
2010-02-22 23:37:19 +01:00
|
|
|
terminal_width = None
|
2010-02-22 23:03:05 +01:00
|
|
|
if sys.stdout.isatty():
|
2010-04-15 17:59:55 +02:00
|
|
|
terminal_width = get_terminal_size()[0]
|
2010-02-22 23:03:05 +01:00
|
|
|
|
|
|
|
print_results(hits, terminal_width=terminal_width)
|
2011-08-02 19:16:18 +02:00
|
|
|
if pypi_hits:
|
|
|
|
return SUCCESS
|
|
|
|
return NO_MATCHES_FOUND
|
2010-02-22 23:03:05 +01:00
|
|
|
|
2014-06-28 00:26:40 +02:00
|
|
|
def search(self, query, options):
|
2020-05-17 22:30:41 +02:00
|
|
|
# type: (List[str], Values) -> List[Dict[str, str]]
|
2014-06-28 00:26:40 +02:00
|
|
|
index_url = options.index
|
2019-09-03 00:48:15 +02:00
|
|
|
|
|
|
|
session = self.get_default_session(options)
|
|
|
|
|
|
|
|
transport = PipXmlrpcTransport(index_url, session)
|
|
|
|
pypi = xmlrpc_client.ServerProxy(index_url, transport)
|
|
|
|
hits = pypi.search({'name': query, 'summary': query}, 'or')
|
|
|
|
return hits
|
2010-02-22 23:03:05 +01:00
|
|
|
|
2010-06-03 04:25:26 +02:00
|
|
|
|
2010-02-23 22:01:43 +01:00
|
|
|
def transform_hits(hits):
|
2020-05-19 09:19:46 +02:00
|
|
|
# type: (List[Dict[str, str]]) -> List[TransformedHit]
|
2010-02-22 23:03:05 +01:00
|
|
|
"""
|
|
|
|
The list from pypi is really a list of versions. We want a list of
|
|
|
|
packages with the list of versions stored inline. This converts the
|
|
|
|
list from pypi into one we can use.
|
|
|
|
"""
|
2020-05-19 09:19:46 +02:00
|
|
|
packages = OrderedDict() # type: OrderedDict[str, TransformedHit]
|
2010-02-22 23:03:05 +01:00
|
|
|
for hit in hits:
|
|
|
|
name = hit['name']
|
|
|
|
summary = hit['summary']
|
|
|
|
version = hit['version']
|
|
|
|
|
|
|
|
if name not in packages.keys():
|
2014-01-27 15:07:10 +01:00
|
|
|
packages[name] = {
|
|
|
|
'name': name,
|
|
|
|
'summary': summary,
|
|
|
|
'versions': [version],
|
|
|
|
}
|
2010-02-22 23:03:05 +01:00
|
|
|
else:
|
|
|
|
packages[name]['versions'].append(version)
|
|
|
|
|
|
|
|
# if this is the highest version, replace summary and score
|
|
|
|
if version == highest_version(packages[name]['versions']):
|
|
|
|
packages[name]['summary'] = summary
|
2016-10-30 20:17:10 +01:00
|
|
|
|
|
|
|
return list(packages.values())
|
2010-02-22 23:03:05 +01:00
|
|
|
|
2010-06-03 04:25:26 +02:00
|
|
|
|
2014-12-21 19:41:23 +01:00
|
|
|
def print_results(hits, name_column_width=None, terminal_width=None):
|
2020-05-19 09:19:46 +02:00
|
|
|
# type: (List[TransformedHit], Optional[int], Optional[int]) -> None
|
2014-12-21 19:41:23 +01:00
|
|
|
if not hits:
|
|
|
|
return
|
|
|
|
if name_column_width is None:
|
2014-12-27 12:11:58 +01:00
|
|
|
name_column_width = max([
|
Fix the latest version number in search results
When listing found packages, `print_results()` uses the last version in
the versions list of a package, but that version is not necessarily the
latest. For example, `SearchCommand.search()` may return the following
(tested with JFrog Artifactory PyPI repo):
pypi_hits = command.search(args, options)
[{'_pypi_ordering': 1,
'name': 'testlib',
'summary': 'Test library.',
'version': '1.0.5'},
{'_pypi_ordering': 0,
'name': 'testlib',
'summary': 'Test library.',
'version': '1.0.3'}]
`pip.commands.search.transform_hits()` preserves the order of versions:
hits = pip.commands.search.transform_hits(pypi_hits)
[{'name': 'testlib',
'summary': 'Test library.',
'versions': ['1.0.5', '1.0.3']}]
`pip.commands.search.print_results()` uses the last version in the list:
pip.commands.search.print_results(hits)
testlib (1.0.3) - Test library.
Expected result:
testlib (1.0.5) - Test library.
`print_results()` uses `highest_version()` already to compare the
installed version with the latest found version (for installed
packages). Same function is used in this fix to show the latest
versions of all found packages.
2017-01-10 00:43:29 +01:00
|
|
|
len(hit['name']) + len(highest_version(hit.get('versions', ['-'])))
|
2014-12-27 12:11:58 +01:00
|
|
|
for hit in hits
|
|
|
|
]) + 4
|
|
|
|
|
2010-02-22 23:03:05 +01:00
|
|
|
installed_packages = [p.project_name for p in pkg_resources.working_set]
|
|
|
|
for hit in hits:
|
|
|
|
name = hit['name']
|
|
|
|
summary = hit['summary'] or ''
|
Fix the latest version number in search results
When listing found packages, `print_results()` uses the last version in
the versions list of a package, but that version is not necessarily the
latest. For example, `SearchCommand.search()` may return the following
(tested with JFrog Artifactory PyPI repo):
pypi_hits = command.search(args, options)
[{'_pypi_ordering': 1,
'name': 'testlib',
'summary': 'Test library.',
'version': '1.0.5'},
{'_pypi_ordering': 0,
'name': 'testlib',
'summary': 'Test library.',
'version': '1.0.3'}]
`pip.commands.search.transform_hits()` preserves the order of versions:
hits = pip.commands.search.transform_hits(pypi_hits)
[{'name': 'testlib',
'summary': 'Test library.',
'versions': ['1.0.5', '1.0.3']}]
`pip.commands.search.print_results()` uses the last version in the list:
pip.commands.search.print_results(hits)
testlib (1.0.3) - Test library.
Expected result:
testlib (1.0.5) - Test library.
`print_results()` uses `highest_version()` already to compare the
installed version with the latest found version (for installed
packages). Same function is used in this fix to show the latest
versions of all found packages.
2017-01-10 00:43:29 +01:00
|
|
|
latest = highest_version(hit.get('versions', ['-']))
|
2010-02-22 23:03:05 +01:00
|
|
|
if terminal_width is not None:
|
2016-05-11 01:07:45 +02:00
|
|
|
target_width = terminal_width - name_column_width - 5
|
|
|
|
if target_width > 10:
|
|
|
|
# wrap and indent summary to fit terminal
|
2020-05-19 09:19:46 +02:00
|
|
|
summary_lines = textwrap.wrap(summary, target_width)
|
|
|
|
summary = ('\n' + ' ' * (name_column_width + 3)).join(
|
|
|
|
summary_lines)
|
2014-01-05 05:00:45 +01:00
|
|
|
|
2020-02-29 20:53:59 +01:00
|
|
|
line = '{name_latest:{name_column_width}} - {summary}'.format(
|
|
|
|
name_latest='{name} ({latest})'.format(**locals()),
|
|
|
|
**locals())
|
2010-02-22 23:03:05 +01:00
|
|
|
try:
|
2019-08-23 02:29:22 +02:00
|
|
|
write_output(line)
|
2010-02-22 23:03:05 +01:00
|
|
|
if name in installed_packages:
|
2020-05-14 20:44:15 +02:00
|
|
|
dist = get_distribution(name)
|
2020-08-04 22:55:56 +02:00
|
|
|
assert dist is not None
|
2014-08-31 01:52:28 +02:00
|
|
|
with indent_log():
|
2010-02-23 00:20:19 +01:00
|
|
|
if dist.version == latest:
|
2019-08-23 02:29:22 +02:00
|
|
|
write_output('INSTALLED: %s (latest)', dist.version)
|
2010-02-23 00:20:19 +01:00
|
|
|
else:
|
2019-08-23 02:29:22 +02:00
|
|
|
write_output('INSTALLED: %s', dist.version)
|
2018-10-26 14:17:01 +02:00
|
|
|
if parse_version(latest).pre:
|
2019-08-23 02:29:22 +02:00
|
|
|
write_output('LATEST: %s (pre-release; install'
|
|
|
|
' with "pip install --pre")', latest)
|
2018-10-26 14:17:01 +02:00
|
|
|
else:
|
2019-08-23 02:29:22 +02:00
|
|
|
write_output('LATEST: %s', latest)
|
2010-02-22 23:03:05 +01:00
|
|
|
except UnicodeEncodeError:
|
|
|
|
pass
|
|
|
|
|
2010-06-03 04:25:26 +02:00
|
|
|
|
2010-02-22 23:03:05 +01:00
|
|
|
def highest_version(versions):
|
2020-05-17 22:30:41 +02:00
|
|
|
# type: (List[str]) -> str
|
2016-05-17 22:32:43 +02:00
|
|
|
return max(versions, key=parse_version)
|