search.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. from __future__ import absolute_import
  2. import logging
  3. import sys
  4. import textwrap
  5. from pip.basecommand import Command, SUCCESS
  6. from pip.compat import OrderedDict
  7. from pip.download import PipXmlrpcTransport
  8. from pip.models import PyPI
  9. from pip.utils import get_terminal_size
  10. from pip.utils.logging import indent_log
  11. from pip.exceptions import CommandError
  12. from pip.status_codes import NO_MATCHES_FOUND
  13. from pip._vendor.packaging.version import parse as parse_version
  14. from pip._vendor import pkg_resources
  15. from pip._vendor.six.moves import xmlrpc_client
  16. logger = logging.getLogger(__name__)
  17. class SearchCommand(Command):
  18. """Search for PyPI packages whose name or summary contains <query>."""
  19. name = 'search'
  20. usage = """
  21. %prog [options] <query>"""
  22. summary = 'Search PyPI for packages.'
  23. def __init__(self, *args, **kw):
  24. super(SearchCommand, self).__init__(*args, **kw)
  25. self.cmd_opts.add_option(
  26. '-i', '--index',
  27. dest='index',
  28. metavar='URL',
  29. default=PyPI.pypi_url,
  30. help='Base URL of Python Package Index (default %default)')
  31. self.parser.insert_option_group(0, self.cmd_opts)
  32. def run(self, options, args):
  33. if not args:
  34. raise CommandError('Missing required argument (search query).')
  35. query = args
  36. pypi_hits = self.search(query, options)
  37. hits = transform_hits(pypi_hits)
  38. terminal_width = None
  39. if sys.stdout.isatty():
  40. terminal_width = get_terminal_size()[0]
  41. print_results(hits, terminal_width=terminal_width)
  42. if pypi_hits:
  43. return SUCCESS
  44. return NO_MATCHES_FOUND
  45. def search(self, query, options):
  46. index_url = options.index
  47. with self._build_session(options) as session:
  48. transport = PipXmlrpcTransport(index_url, session)
  49. pypi = xmlrpc_client.ServerProxy(index_url, transport)
  50. hits = pypi.search({'name': query, 'summary': query}, 'or')
  51. return hits
  52. def transform_hits(hits):
  53. """
  54. The list from pypi is really a list of versions. We want a list of
  55. packages with the list of versions stored inline. This converts the
  56. list from pypi into one we can use.
  57. """
  58. packages = OrderedDict()
  59. for hit in hits:
  60. name = hit['name']
  61. summary = hit['summary']
  62. version = hit['version']
  63. if name not in packages.keys():
  64. packages[name] = {
  65. 'name': name,
  66. 'summary': summary,
  67. 'versions': [version],
  68. }
  69. else:
  70. packages[name]['versions'].append(version)
  71. # if this is the highest version, replace summary and score
  72. if version == highest_version(packages[name]['versions']):
  73. packages[name]['summary'] = summary
  74. return list(packages.values())
  75. def print_results(hits, name_column_width=None, terminal_width=None):
  76. if not hits:
  77. return
  78. if name_column_width is None:
  79. name_column_width = max([
  80. len(hit['name']) + len(hit.get('versions', ['-'])[-1])
  81. for hit in hits
  82. ]) + 4
  83. installed_packages = [p.project_name for p in pkg_resources.working_set]
  84. for hit in hits:
  85. name = hit['name']
  86. summary = hit['summary'] or ''
  87. version = hit.get('versions', ['-'])[-1]
  88. if terminal_width is not None:
  89. target_width = terminal_width - name_column_width - 5
  90. if target_width > 10:
  91. # wrap and indent summary to fit terminal
  92. summary = textwrap.wrap(summary, target_width)
  93. summary = ('\n' + ' ' * (name_column_width + 3)).join(summary)
  94. line = '%-*s - %s' % (name_column_width,
  95. '%s (%s)' % (name, version), summary)
  96. try:
  97. logger.info(line)
  98. if name in installed_packages:
  99. dist = pkg_resources.get_distribution(name)
  100. with indent_log():
  101. latest = highest_version(hit['versions'])
  102. if dist.version == latest:
  103. logger.info('INSTALLED: %s (latest)', dist.version)
  104. else:
  105. logger.info('INSTALLED: %s', dist.version)
  106. logger.info('LATEST: %s', latest)
  107. except UnicodeEncodeError:
  108. pass
  109. def highest_version(versions):
  110. return max(versions, key=parse_version)