search.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. import sys
  2. import textwrap
  3. import pip.download
  4. from pip.basecommand import Command, SUCCESS
  5. from pip.util import get_terminal_size
  6. from pip.log import logger
  7. from pip.backwardcompat import xmlrpclib, reduce, cmp
  8. from pip.exceptions import CommandError
  9. from pip.status_codes import NO_MATCHES_FOUND
  10. from pip._vendor import pkg_resources
  11. from distutils.version import StrictVersion, LooseVersion
  12. class SearchCommand(Command):
  13. """Search for PyPI packages whose name or summary contains <query>."""
  14. name = 'search'
  15. usage = """
  16. %prog [options] <query>"""
  17. summary = 'Search PyPI for packages.'
  18. def __init__(self, *args, **kw):
  19. super(SearchCommand, self).__init__(*args, **kw)
  20. self.cmd_opts.add_option(
  21. '--index',
  22. dest='index',
  23. metavar='URL',
  24. default='https://pypi.python.org/pypi',
  25. help='Base URL of Python Package Index (default %default)')
  26. self.parser.insert_option_group(0, self.cmd_opts)
  27. def run(self, options, args):
  28. if not args:
  29. raise CommandError('Missing required argument (search query).')
  30. query = args
  31. index_url = options.index
  32. pypi_hits = self.search(query, index_url)
  33. hits = transform_hits(pypi_hits)
  34. terminal_width = None
  35. if sys.stdout.isatty():
  36. terminal_width = get_terminal_size()[0]
  37. print_results(hits, terminal_width=terminal_width)
  38. if pypi_hits:
  39. return SUCCESS
  40. return NO_MATCHES_FOUND
  41. def search(self, query, index_url):
  42. pypi = xmlrpclib.ServerProxy(index_url)
  43. hits = pypi.search({'name': query, 'summary': query}, 'or')
  44. return hits
  45. def transform_hits(hits):
  46. """
  47. The list from pypi is really a list of versions. We want a list of
  48. packages with the list of versions stored inline. This converts the
  49. list from pypi into one we can use.
  50. """
  51. packages = {}
  52. for hit in hits:
  53. name = hit['name']
  54. summary = hit['summary']
  55. version = hit['version']
  56. score = hit['_pypi_ordering']
  57. if score is None:
  58. score = 0
  59. if name not in packages.keys():
  60. packages[name] = {'name': name, 'summary': summary, 'versions': [version], 'score': score}
  61. else:
  62. packages[name]['versions'].append(version)
  63. # if this is the highest version, replace summary and score
  64. if version == highest_version(packages[name]['versions']):
  65. packages[name]['summary'] = summary
  66. packages[name]['score'] = score
  67. # each record has a unique name now, so we will convert the dict into a list sorted by score
  68. package_list = sorted(packages.values(), key=lambda x: x['score'], reverse=True)
  69. return package_list
  70. def print_results(hits, name_column_width=25, terminal_width=None):
  71. installed_packages = [p.project_name for p in pkg_resources.working_set]
  72. for hit in hits:
  73. name = hit['name']
  74. summary = hit['summary'] or ''
  75. if terminal_width is not None:
  76. # wrap and indent summary to fit terminal
  77. summary = textwrap.wrap(summary, terminal_width - name_column_width - 5)
  78. summary = ('\n' + ' ' * (name_column_width + 3)).join(summary)
  79. line = '%s - %s' % (name.ljust(name_column_width), summary)
  80. try:
  81. logger.notify(line)
  82. if name in installed_packages:
  83. dist = pkg_resources.get_distribution(name)
  84. logger.indent += 2
  85. try:
  86. latest = highest_version(hit['versions'])
  87. if dist.version == latest:
  88. logger.notify('INSTALLED: %s (latest)' % dist.version)
  89. else:
  90. logger.notify('INSTALLED: %s' % dist.version)
  91. logger.notify('LATEST: %s' % latest)
  92. finally:
  93. logger.indent -= 2
  94. except UnicodeEncodeError:
  95. pass
  96. def compare_versions(version1, version2):
  97. try:
  98. return cmp(StrictVersion(version1), StrictVersion(version2))
  99. # in case of abnormal version number, fall back to LooseVersion
  100. except ValueError:
  101. pass
  102. try:
  103. return cmp(LooseVersion(version1), LooseVersion(version2))
  104. except TypeError:
  105. # certain LooseVersion comparions raise due to unorderable types,
  106. # fallback to string comparison
  107. return cmp([str(v) for v in LooseVersion(version1).version],
  108. [str(v) for v in LooseVersion(version2).version])
  109. def highest_version(versions):
  110. return reduce((lambda v1, v2: compare_versions(v1, v2) == 1 and v1 or v2), versions)