You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

140 lines
4.5 KiB

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