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.

215 lines
6.8 KiB

4 years ago
  1. """Discover and load entry points from installed packages."""
  2. # Copyright (c) Thomas Kluyver and contributors
  3. # Distributed under the terms of the MIT license; see LICENSE file.
  4. from contextlib import contextmanager
  5. import glob
  6. from importlib import import_module
  7. import io
  8. import itertools
  9. import os.path as osp
  10. import re
  11. import sys
  12. import warnings
  13. import zipfile
  14. if sys.version_info[0] >= 3:
  15. import configparser
  16. else:
  17. from backports import configparser
  18. entry_point_pattern = re.compile(r"""
  19. (?P<modulename>\w+(\.\w+)*)
  20. (:(?P<objectname>\w+(\.\w+)*))?
  21. \s*
  22. (\[(?P<extras>.+)\])?
  23. $
  24. """, re.VERBOSE)
  25. __version__ = '0.2.3'
  26. class BadEntryPoint(Exception):
  27. """Raised when an entry point can't be parsed.
  28. """
  29. def __init__(self, epstr):
  30. self.epstr = epstr
  31. def __str__(self):
  32. return "Couldn't parse entry point spec: %r" % self.epstr
  33. @staticmethod
  34. @contextmanager
  35. def err_to_warnings():
  36. try:
  37. yield
  38. except BadEntryPoint as e:
  39. warnings.warn(str(e))
  40. class NoSuchEntryPoint(Exception):
  41. """Raised by :func:`get_single` when no matching entry point is found."""
  42. def __init__(self, group, name):
  43. self.group = group
  44. self.name = name
  45. def __str__(self):
  46. return "No {!r} entry point found in group {!r}".format(self.name, self.group)
  47. class CaseSensitiveConfigParser(configparser.ConfigParser):
  48. optionxform = staticmethod(str)
  49. class EntryPoint(object):
  50. def __init__(self, name, module_name, object_name, extras=None, distro=None):
  51. self.name = name
  52. self.module_name = module_name
  53. self.object_name = object_name
  54. self.extras = extras
  55. self.distro = distro
  56. def __repr__(self):
  57. return "EntryPoint(%r, %r, %r, %r)" % \
  58. (self.name, self.module_name, self.object_name, self.distro)
  59. def load(self):
  60. """Load the object to which this entry point refers.
  61. """
  62. mod = import_module(self.module_name)
  63. obj = mod
  64. if self.object_name:
  65. for attr in self.object_name.split('.'):
  66. obj = getattr(obj, attr)
  67. return obj
  68. @classmethod
  69. def from_string(cls, epstr, name, distro=None):
  70. """Parse an entry point from the syntax in entry_points.txt
  71. :param str epstr: The entry point string (not including 'name =')
  72. :param str name: The name of this entry point
  73. :param Distribution distro: The distribution in which the entry point was found
  74. :rtype: EntryPoint
  75. :raises BadEntryPoint: if *epstr* can't be parsed as an entry point.
  76. """
  77. m = entry_point_pattern.match(epstr)
  78. if m:
  79. mod, obj, extras = m.group('modulename', 'objectname', 'extras')
  80. if extras is not None:
  81. extras = re.split(',\s*', extras)
  82. return cls(name, mod, obj, extras, distro)
  83. else:
  84. raise BadEntryPoint(epstr)
  85. class Distribution(object):
  86. def __init__(self, name, version):
  87. self.name = name
  88. self.version = version
  89. def __repr__(self):
  90. return "Distribution(%r, %r)" % (self.name, self.version)
  91. def iter_files_distros(path=None, repeated_distro='first'):
  92. if path is None:
  93. path = sys.path
  94. # Distributions found earlier in path will shadow those with the same name
  95. # found later. If these distributions used different module names, it may
  96. # actually be possible to import both, but in most cases this shadowing
  97. # will be correct.
  98. distro_names_seen = set()
  99. for folder in path:
  100. if folder.rstrip('/\\').endswith('.egg'):
  101. # Gah, eggs
  102. egg_name = osp.basename(folder)
  103. if '-' in egg_name:
  104. distro = Distribution(*egg_name.split('-')[:2])
  105. if (repeated_distro == 'first') \
  106. and (distro.name in distro_names_seen):
  107. continue
  108. distro_names_seen.add(distro.name)
  109. else:
  110. distro = None
  111. if osp.isdir(folder):
  112. ep_path = osp.join(folder, 'EGG-INFO', 'entry_points.txt')
  113. if osp.isfile(ep_path):
  114. cp = CaseSensitiveConfigParser()
  115. cp.read(ep_path)
  116. yield cp, distro
  117. elif zipfile.is_zipfile(folder):
  118. z = zipfile.ZipFile(folder)
  119. try:
  120. info = z.getinfo('EGG-INFO/entry_points.txt')
  121. except KeyError:
  122. continue
  123. cp = CaseSensitiveConfigParser()
  124. with z.open(info) as f:
  125. fu = io.TextIOWrapper(f)
  126. cp.read_file(fu,
  127. source=osp.join(folder, 'EGG-INFO', 'entry_points.txt'))
  128. yield cp, distro
  129. for path in itertools.chain(
  130. glob.iglob(osp.join(folder, '*.dist-info', 'entry_points.txt')),
  131. glob.iglob(osp.join(folder, '*.egg-info', 'entry_points.txt'))
  132. ):
  133. distro_name_version = osp.splitext(osp.basename(osp.dirname(path)))[0]
  134. if '-' in distro_name_version:
  135. distro = Distribution(*distro_name_version.split('-', 1))
  136. if (repeated_distro == 'first') \
  137. and (distro.name in distro_names_seen):
  138. continue
  139. distro_names_seen.add(distro.name)
  140. else:
  141. distro = None
  142. cp = CaseSensitiveConfigParser()
  143. cp.read(path)
  144. yield cp, distro
  145. def get_single(group, name, path=None):
  146. """Find a single entry point.
  147. Returns an :class:`EntryPoint` object, or raises :exc:`NoSuchEntryPoint`
  148. if no match is found.
  149. """
  150. for config, distro in iter_files_distros(path=path):
  151. if (group in config) and (name in config[group]):
  152. epstr = config[group][name]
  153. with BadEntryPoint.err_to_warnings():
  154. return EntryPoint.from_string(epstr, name, distro)
  155. raise NoSuchEntryPoint(group, name)
  156. def get_group_named(group, path=None):
  157. """Find a group of entry points with unique names.
  158. Returns a dictionary of names to :class:`EntryPoint` objects.
  159. """
  160. result = {}
  161. for ep in get_group_all(group, path=path):
  162. if ep.name not in result:
  163. result[ep.name] = ep
  164. return result
  165. def get_group_all(group, path=None):
  166. """Find all entry points in a group.
  167. Returns a list of :class:`EntryPoint` objects.
  168. """
  169. result = []
  170. for config, distro in iter_files_distros(path=path):
  171. if group in config:
  172. for name, epstr in config[group].items():
  173. with BadEntryPoint.err_to_warnings():
  174. result.append(EntryPoint.from_string(epstr, name, distro))
  175. return result
  176. if __name__ == '__main__':
  177. import pprint
  178. pprint.pprint(get_group_all('console_scripts'))