extension.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  2. # not use this file except in compliance with the License. You may obtain
  3. # a copy of the License at
  4. #
  5. # http://www.apache.org/licenses/LICENSE-2.0
  6. #
  7. # Unless required by applicable law or agreed to in writing, software
  8. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  9. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  10. # License for the specific language governing permissions and limitations
  11. # under the License.
  12. """ExtensionManager
  13. """
  14. import logging
  15. import operator
  16. from . import _cache
  17. from .exception import NoMatches
  18. LOG = logging.getLogger(__name__)
  19. class Extension(object):
  20. """Book-keeping object for tracking extensions.
  21. The arguments passed to the constructor are saved as attributes of
  22. the instance using the same names, and can be accessed by the
  23. callables passed to :meth:`map` or when iterating over an
  24. :class:`ExtensionManager` directly.
  25. :param name: The entry point name.
  26. :type name: str
  27. :param entry_point: The EntryPoint instance returned by
  28. :mod:`entrypoints`.
  29. :type entry_point: EntryPoint
  30. :param plugin: The value returned by entry_point.load()
  31. :param obj: The object returned by ``plugin(*args, **kwds)`` if the
  32. manager invoked the extension on load.
  33. """
  34. def __init__(self, name, entry_point, plugin, obj):
  35. self.name = name
  36. self.entry_point = entry_point
  37. self.plugin = plugin
  38. self.obj = obj
  39. @property
  40. def module_name(self):
  41. """The name of the module from which the entry point is loaded.
  42. :return: A string in 'dotted.module' format.
  43. """
  44. # NOTE: importlib_metadata from PyPI includes this but the
  45. # Python 3.8 standard library does not.
  46. match = self.entry_point.pattern.match(self.entry_point.value)
  47. return match.group('module')
  48. @property
  49. def attr(self):
  50. """The attribute of the module to be loaded."""
  51. match = self.entry_point.pattern.match(self.entry_point.value)
  52. return match.group('attr')
  53. @property
  54. def entry_point_target(self):
  55. """The module and attribute referenced by this extension's entry_point.
  56. :return: A string representation of the target of the entry point in
  57. 'dotted.module:object' format.
  58. """
  59. return self.entry_point.value
  60. class ExtensionManager(object):
  61. """Base class for all of the other managers.
  62. :param namespace: The namespace for the entry points.
  63. :type namespace: str
  64. :param invoke_on_load: Boolean controlling whether to invoke the
  65. object returned by the entry point after the driver is loaded.
  66. :type invoke_on_load: bool
  67. :param invoke_args: Positional arguments to pass when invoking
  68. the object returned by the entry point. Only used if invoke_on_load
  69. is True.
  70. :type invoke_args: tuple
  71. :param invoke_kwds: Named arguments to pass when invoking
  72. the object returned by the entry point. Only used if invoke_on_load
  73. is True.
  74. :type invoke_kwds: dict
  75. :param propagate_map_exceptions: Boolean controlling whether exceptions
  76. are propagated up through the map call or whether they are logged and
  77. then ignored
  78. :type propagate_map_exceptions: bool
  79. :param on_load_failure_callback: Callback function that will be called when
  80. an entrypoint can not be loaded. The arguments that will be provided
  81. when this is called (when an entrypoint fails to load) are
  82. (manager, entrypoint, exception)
  83. :type on_load_failure_callback: function
  84. :param verify_requirements: Use setuptools to enforce the
  85. dependencies of the plugin(s) being loaded. Defaults to False.
  86. :type verify_requirements: bool
  87. """
  88. def __init__(self, namespace,
  89. invoke_on_load=False,
  90. invoke_args=(),
  91. invoke_kwds={},
  92. propagate_map_exceptions=False,
  93. on_load_failure_callback=None,
  94. verify_requirements=False):
  95. self._init_attributes(
  96. namespace,
  97. propagate_map_exceptions=propagate_map_exceptions,
  98. on_load_failure_callback=on_load_failure_callback)
  99. extensions = self._load_plugins(invoke_on_load,
  100. invoke_args,
  101. invoke_kwds,
  102. verify_requirements)
  103. self._init_plugins(extensions)
  104. @classmethod
  105. def make_test_instance(cls, extensions, namespace='TESTING',
  106. propagate_map_exceptions=False,
  107. on_load_failure_callback=None,
  108. verify_requirements=False):
  109. """Construct a test ExtensionManager
  110. Test instances are passed a list of extensions to work from rather
  111. than loading them from entry points.
  112. :param extensions: Pre-configured Extension instances to use
  113. :type extensions: list of :class:`~stevedore.extension.Extension`
  114. :param namespace: The namespace for the manager; used only for
  115. identification since the extensions are passed in.
  116. :type namespace: str
  117. :param propagate_map_exceptions: When calling map, controls whether
  118. exceptions are propagated up through the map call or whether they
  119. are logged and then ignored
  120. :type propagate_map_exceptions: bool
  121. :param on_load_failure_callback: Callback function that will
  122. be called when an entrypoint can not be loaded. The
  123. arguments that will be provided when this is called (when
  124. an entrypoint fails to load) are (manager, entrypoint,
  125. exception)
  126. :type on_load_failure_callback: function
  127. :param verify_requirements: Use setuptools to enforce the
  128. dependencies of the plugin(s) being loaded. Defaults to False.
  129. :type verify_requirements: bool
  130. :return: The manager instance, initialized for testing
  131. """
  132. o = cls.__new__(cls)
  133. o._init_attributes(namespace,
  134. propagate_map_exceptions=propagate_map_exceptions,
  135. on_load_failure_callback=on_load_failure_callback)
  136. o._init_plugins(extensions)
  137. return o
  138. def _init_attributes(self, namespace, propagate_map_exceptions=False,
  139. on_load_failure_callback=None):
  140. self.namespace = namespace
  141. self.propagate_map_exceptions = propagate_map_exceptions
  142. self._on_load_failure_callback = on_load_failure_callback
  143. def _init_plugins(self, extensions):
  144. self.extensions = extensions
  145. self._extensions_by_name_cache = None
  146. @property
  147. def _extensions_by_name(self):
  148. if self._extensions_by_name_cache is None:
  149. d = {}
  150. for e in self.extensions:
  151. d[e.name] = e
  152. self._extensions_by_name_cache = d
  153. return self._extensions_by_name_cache
  154. ENTRY_POINT_CACHE = {}
  155. def list_entry_points(self):
  156. """Return the list of entry points for this namespace.
  157. The entry points are not actually loaded, their list is just read and
  158. returned.
  159. """
  160. if self.namespace not in self.ENTRY_POINT_CACHE:
  161. eps = list(_cache.get_group_all(self.namespace))
  162. self.ENTRY_POINT_CACHE[self.namespace] = eps
  163. return self.ENTRY_POINT_CACHE[self.namespace]
  164. def entry_points_names(self):
  165. """Return the list of entry points names for this namespace."""
  166. return list(map(operator.attrgetter("name"), self.list_entry_points()))
  167. def _load_plugins(self, invoke_on_load, invoke_args, invoke_kwds,
  168. verify_requirements):
  169. extensions = []
  170. for ep in self.list_entry_points():
  171. LOG.debug('found extension %r', ep)
  172. try:
  173. ext = self._load_one_plugin(ep,
  174. invoke_on_load,
  175. invoke_args,
  176. invoke_kwds,
  177. verify_requirements,
  178. )
  179. if ext:
  180. extensions.append(ext)
  181. except (KeyboardInterrupt, AssertionError):
  182. raise
  183. except Exception as err:
  184. if self._on_load_failure_callback is not None:
  185. self._on_load_failure_callback(self, ep, err)
  186. else:
  187. # Log the reason we couldn't import the module,
  188. # usually without a traceback. The most common
  189. # reason is an ImportError due to a missing
  190. # dependency, and the error message should be
  191. # enough to debug that. If debug logging is
  192. # enabled for our logger, provide the full
  193. # traceback.
  194. LOG.error('Could not load %r: %s', ep.name, err,
  195. exc_info=LOG.isEnabledFor(logging.DEBUG))
  196. return extensions
  197. def _load_one_plugin(self, ep, invoke_on_load, invoke_args, invoke_kwds,
  198. verify_requirements):
  199. # NOTE(dhellmann): Using require=False is deprecated in
  200. # setuptools 11.3.
  201. if hasattr(ep, 'resolve') and hasattr(ep, 'require'):
  202. if verify_requirements:
  203. ep.require()
  204. plugin = ep.resolve()
  205. else:
  206. plugin = ep.load()
  207. if invoke_on_load:
  208. obj = plugin(*invoke_args, **invoke_kwds)
  209. else:
  210. obj = None
  211. return Extension(ep.name, ep, plugin, obj)
  212. def names(self):
  213. "Returns the names of the discovered extensions"
  214. # We want to return the names of the extensions in the order
  215. # they would be used by map(), since some subclasses change
  216. # that order.
  217. return [e.name for e in self.extensions]
  218. def map(self, func, *args, **kwds):
  219. """Iterate over the extensions invoking func() for each.
  220. The signature for func() should be::
  221. def func(ext, *args, **kwds):
  222. pass
  223. The first argument to func(), 'ext', is the
  224. :class:`~stevedore.extension.Extension` instance.
  225. Exceptions raised from within func() are propagated up and
  226. processing stopped if self.propagate_map_exceptions is True,
  227. otherwise they are logged and ignored.
  228. :param func: Callable to invoke for each extension.
  229. :param args: Variable arguments to pass to func()
  230. :param kwds: Keyword arguments to pass to func()
  231. :returns: List of values returned from func()
  232. """
  233. if not self.extensions:
  234. # FIXME: Use a more specific exception class here.
  235. raise NoMatches('No %s extensions found' % self.namespace)
  236. response = []
  237. for e in self.extensions:
  238. self._invoke_one_plugin(response.append, func, e, args, kwds)
  239. return response
  240. @staticmethod
  241. def _call_extension_method(extension, method_name, *args, **kwds):
  242. return getattr(extension.obj, method_name)(*args, **kwds)
  243. def map_method(self, method_name, *args, **kwds):
  244. """Iterate over the extensions invoking a method by name.
  245. This is equivalent of using :meth:`map` with func set to
  246. `lambda x: x.obj.method_name()`
  247. while being more convenient.
  248. Exceptions raised from within the called method are propagated up
  249. and processing stopped if self.propagate_map_exceptions is True,
  250. otherwise they are logged and ignored.
  251. .. versionadded:: 0.12
  252. :param method_name: The extension method name
  253. to call for each extension.
  254. :param args: Variable arguments to pass to method
  255. :param kwds: Keyword arguments to pass to method
  256. :returns: List of values returned from methods
  257. """
  258. return self.map(self._call_extension_method,
  259. method_name, *args, **kwds)
  260. def _invoke_one_plugin(self, response_callback, func, e, args, kwds):
  261. try:
  262. response_callback(func(e, *args, **kwds))
  263. except Exception as err:
  264. if self.propagate_map_exceptions:
  265. raise
  266. else:
  267. LOG.error('error calling %r: %s', e.name, err)
  268. LOG.exception(err)
  269. def items(self):
  270. """Return an iterator of tuples of the form (name, extension).
  271. This is analogous to the Mapping.items() method.
  272. """
  273. return self._extensions_by_name.items()
  274. def __iter__(self):
  275. """Produce iterator for the manager.
  276. Iterating over an ExtensionManager produces the :class:`Extension`
  277. instances in the order they would be invoked.
  278. """
  279. return iter(self.extensions)
  280. def __getitem__(self, name):
  281. """Return the named extension.
  282. Accessing an ExtensionManager as a dictionary (``em['name']``)
  283. produces the :class:`Extension` instance with the
  284. specified name.
  285. """
  286. return self._extensions_by_name[name]
  287. def __contains__(self, name):
  288. """Return true if name is in list of enabled extensions."""
  289. return any(extension.name == name for extension in self.extensions)