comparison env/lib/python3.9/site-packages/pluggy/manager.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
1 import inspect
2 import sys
3 from . import _tracing
4 from .callers import _Result
5 from .hooks import HookImpl, _HookRelay, _HookCaller, normalize_hookimpl_opts
6 import warnings
7
8 if sys.version_info >= (3, 8):
9 from importlib import metadata as importlib_metadata
10 else:
11 import importlib_metadata
12
13
14 def _warn_for_function(warning, function):
15 warnings.warn_explicit(
16 warning,
17 type(warning),
18 lineno=function.__code__.co_firstlineno,
19 filename=function.__code__.co_filename,
20 )
21
22
23 class PluginValidationError(Exception):
24 """ plugin failed validation.
25
26 :param object plugin: the plugin which failed validation,
27 may be a module or an arbitrary object.
28 """
29
30 def __init__(self, plugin, message):
31 self.plugin = plugin
32 super(Exception, self).__init__(message)
33
34
35 class DistFacade(object):
36 """Emulate a pkg_resources Distribution"""
37
38 def __init__(self, dist):
39 self._dist = dist
40
41 @property
42 def project_name(self):
43 return self.metadata["name"]
44
45 def __getattr__(self, attr, default=None):
46 return getattr(self._dist, attr, default)
47
48 def __dir__(self):
49 return sorted(dir(self._dist) + ["_dist", "project_name"])
50
51
52 class PluginManager(object):
53 """ Core :py:class:`.PluginManager` class which manages registration
54 of plugin objects and 1:N hook calling.
55
56 You can register new hooks by calling :py:meth:`add_hookspecs(module_or_class)
57 <.PluginManager.add_hookspecs>`.
58 You can register plugin objects (which contain hooks) by calling
59 :py:meth:`register(plugin) <.PluginManager.register>`. The :py:class:`.PluginManager`
60 is initialized with a prefix that is searched for in the names of the dict
61 of registered plugin objects.
62
63 For debugging purposes you can call :py:meth:`.PluginManager.enable_tracing`
64 which will subsequently send debug information to the trace helper.
65 """
66
67 def __init__(self, project_name, implprefix=None):
68 """If ``implprefix`` is given implementation functions
69 will be recognized if their name matches the ``implprefix``. """
70 self.project_name = project_name
71 self._name2plugin = {}
72 self._plugin2hookcallers = {}
73 self._plugin_distinfo = []
74 self.trace = _tracing.TagTracer().get("pluginmanage")
75 self.hook = _HookRelay()
76 if implprefix is not None:
77 warnings.warn(
78 "Support for the `implprefix` arg is now deprecated and will "
79 "be removed in an upcoming release. Please use HookimplMarker.",
80 DeprecationWarning,
81 stacklevel=2,
82 )
83 self._implprefix = implprefix
84 self._inner_hookexec = lambda hook, methods, kwargs: hook.multicall(
85 methods,
86 kwargs,
87 firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
88 )
89
90 def _hookexec(self, hook, methods, kwargs):
91 # called from all hookcaller instances.
92 # enable_tracing will set its own wrapping function at self._inner_hookexec
93 return self._inner_hookexec(hook, methods, kwargs)
94
95 def register(self, plugin, name=None):
96 """ Register a plugin and return its canonical name or ``None`` if the name
97 is blocked from registering. Raise a :py:class:`ValueError` if the plugin
98 is already registered. """
99 plugin_name = name or self.get_canonical_name(plugin)
100
101 if plugin_name in self._name2plugin or plugin in self._plugin2hookcallers:
102 if self._name2plugin.get(plugin_name, -1) is None:
103 return # blocked plugin, return None to indicate no registration
104 raise ValueError(
105 "Plugin already registered: %s=%s\n%s"
106 % (plugin_name, plugin, self._name2plugin)
107 )
108
109 # XXX if an error happens we should make sure no state has been
110 # changed at point of return
111 self._name2plugin[plugin_name] = plugin
112
113 # register matching hook implementations of the plugin
114 self._plugin2hookcallers[plugin] = hookcallers = []
115 for name in dir(plugin):
116 hookimpl_opts = self.parse_hookimpl_opts(plugin, name)
117 if hookimpl_opts is not None:
118 normalize_hookimpl_opts(hookimpl_opts)
119 method = getattr(plugin, name)
120 hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts)
121 hook = getattr(self.hook, name, None)
122 if hook is None:
123 hook = _HookCaller(name, self._hookexec)
124 setattr(self.hook, name, hook)
125 elif hook.has_spec():
126 self._verify_hook(hook, hookimpl)
127 hook._maybe_apply_history(hookimpl)
128 hook._add_hookimpl(hookimpl)
129 hookcallers.append(hook)
130 return plugin_name
131
132 def parse_hookimpl_opts(self, plugin, name):
133 method = getattr(plugin, name)
134 if not inspect.isroutine(method):
135 return
136 try:
137 res = getattr(method, self.project_name + "_impl", None)
138 except Exception:
139 res = {}
140 if res is not None and not isinstance(res, dict):
141 # false positive
142 res = None
143 # TODO: remove when we drop implprefix in 1.0
144 elif res is None and self._implprefix and name.startswith(self._implprefix):
145 _warn_for_function(
146 DeprecationWarning(
147 "The `implprefix` system is deprecated please decorate "
148 "this function using an instance of HookimplMarker."
149 ),
150 method,
151 )
152 res = {}
153 return res
154
155 def unregister(self, plugin=None, name=None):
156 """ unregister a plugin object and all its contained hook implementations
157 from internal data structures. """
158 if name is None:
159 assert plugin is not None, "one of name or plugin needs to be specified"
160 name = self.get_name(plugin)
161
162 if plugin is None:
163 plugin = self.get_plugin(name)
164
165 # if self._name2plugin[name] == None registration was blocked: ignore
166 if self._name2plugin.get(name):
167 del self._name2plugin[name]
168
169 for hookcaller in self._plugin2hookcallers.pop(plugin, []):
170 hookcaller._remove_plugin(plugin)
171
172 return plugin
173
174 def set_blocked(self, name):
175 """ block registrations of the given name, unregister if already registered. """
176 self.unregister(name=name)
177 self._name2plugin[name] = None
178
179 def is_blocked(self, name):
180 """ return ``True`` if the given plugin name is blocked. """
181 return name in self._name2plugin and self._name2plugin[name] is None
182
183 def add_hookspecs(self, module_or_class):
184 """ add new hook specifications defined in the given ``module_or_class``.
185 Functions are recognized if they have been decorated accordingly. """
186 names = []
187 for name in dir(module_or_class):
188 spec_opts = self.parse_hookspec_opts(module_or_class, name)
189 if spec_opts is not None:
190 hc = getattr(self.hook, name, None)
191 if hc is None:
192 hc = _HookCaller(name, self._hookexec, module_or_class, spec_opts)
193 setattr(self.hook, name, hc)
194 else:
195 # plugins registered this hook without knowing the spec
196 hc.set_specification(module_or_class, spec_opts)
197 for hookfunction in hc.get_hookimpls():
198 self._verify_hook(hc, hookfunction)
199 names.append(name)
200
201 if not names:
202 raise ValueError(
203 "did not find any %r hooks in %r" % (self.project_name, module_or_class)
204 )
205
206 def parse_hookspec_opts(self, module_or_class, name):
207 method = getattr(module_or_class, name)
208 return getattr(method, self.project_name + "_spec", None)
209
210 def get_plugins(self):
211 """ return the set of registered plugins. """
212 return set(self._plugin2hookcallers)
213
214 def is_registered(self, plugin):
215 """ Return ``True`` if the plugin is already registered. """
216 return plugin in self._plugin2hookcallers
217
218 def get_canonical_name(self, plugin):
219 """ Return canonical name for a plugin object. Note that a plugin
220 may be registered under a different name which was specified
221 by the caller of :py:meth:`register(plugin, name) <.PluginManager.register>`.
222 To obtain the name of an registered plugin use :py:meth:`get_name(plugin)
223 <.PluginManager.get_name>` instead."""
224 return getattr(plugin, "__name__", None) or str(id(plugin))
225
226 def get_plugin(self, name):
227 """ Return a plugin or ``None`` for the given name. """
228 return self._name2plugin.get(name)
229
230 def has_plugin(self, name):
231 """ Return ``True`` if a plugin with the given name is registered. """
232 return self.get_plugin(name) is not None
233
234 def get_name(self, plugin):
235 """ Return name for registered plugin or ``None`` if not registered. """
236 for name, val in self._name2plugin.items():
237 if plugin == val:
238 return name
239
240 def _verify_hook(self, hook, hookimpl):
241 if hook.is_historic() and hookimpl.hookwrapper:
242 raise PluginValidationError(
243 hookimpl.plugin,
244 "Plugin %r\nhook %r\nhistoric incompatible to hookwrapper"
245 % (hookimpl.plugin_name, hook.name),
246 )
247 if hook.spec.warn_on_impl:
248 _warn_for_function(hook.spec.warn_on_impl, hookimpl.function)
249 # positional arg checking
250 notinspec = set(hookimpl.argnames) - set(hook.spec.argnames)
251 if notinspec:
252 raise PluginValidationError(
253 hookimpl.plugin,
254 "Plugin %r for hook %r\nhookimpl definition: %s\n"
255 "Argument(s) %s are declared in the hookimpl but "
256 "can not be found in the hookspec"
257 % (
258 hookimpl.plugin_name,
259 hook.name,
260 _formatdef(hookimpl.function),
261 notinspec,
262 ),
263 )
264
265 def check_pending(self):
266 """ Verify that all hooks which have not been verified against
267 a hook specification are optional, otherwise raise :py:class:`.PluginValidationError`."""
268 for name in self.hook.__dict__:
269 if name[0] != "_":
270 hook = getattr(self.hook, name)
271 if not hook.has_spec():
272 for hookimpl in hook.get_hookimpls():
273 if not hookimpl.optionalhook:
274 raise PluginValidationError(
275 hookimpl.plugin,
276 "unknown hook %r in plugin %r"
277 % (name, hookimpl.plugin),
278 )
279
280 def load_setuptools_entrypoints(self, group, name=None):
281 """ Load modules from querying the specified setuptools ``group``.
282
283 :param str group: entry point group to load plugins
284 :param str name: if given, loads only plugins with the given ``name``.
285 :rtype: int
286 :return: return the number of loaded plugins by this call.
287 """
288 count = 0
289 for dist in importlib_metadata.distributions():
290 for ep in dist.entry_points:
291 if (
292 ep.group != group
293 or (name is not None and ep.name != name)
294 # already registered
295 or self.get_plugin(ep.name)
296 or self.is_blocked(ep.name)
297 ):
298 continue
299 plugin = ep.load()
300 self.register(plugin, name=ep.name)
301 self._plugin_distinfo.append((plugin, DistFacade(dist)))
302 count += 1
303 return count
304
305 def list_plugin_distinfo(self):
306 """ return list of distinfo/plugin tuples for all setuptools registered
307 plugins. """
308 return list(self._plugin_distinfo)
309
310 def list_name_plugin(self):
311 """ return list of name/plugin pairs. """
312 return list(self._name2plugin.items())
313
314 def get_hookcallers(self, plugin):
315 """ get all hook callers for the specified plugin. """
316 return self._plugin2hookcallers.get(plugin)
317
318 def add_hookcall_monitoring(self, before, after):
319 """ add before/after tracing functions for all hooks
320 and return an undo function which, when called,
321 will remove the added tracers.
322
323 ``before(hook_name, hook_impls, kwargs)`` will be called ahead
324 of all hook calls and receive a hookcaller instance, a list
325 of HookImpl instances and the keyword arguments for the hook call.
326
327 ``after(outcome, hook_name, hook_impls, kwargs)`` receives the
328 same arguments as ``before`` but also a :py:class:`pluggy.callers._Result` object
329 which represents the result of the overall hook call.
330 """
331 oldcall = self._inner_hookexec
332
333 def traced_hookexec(hook, hook_impls, kwargs):
334 before(hook.name, hook_impls, kwargs)
335 outcome = _Result.from_call(lambda: oldcall(hook, hook_impls, kwargs))
336 after(outcome, hook.name, hook_impls, kwargs)
337 return outcome.get_result()
338
339 self._inner_hookexec = traced_hookexec
340
341 def undo():
342 self._inner_hookexec = oldcall
343
344 return undo
345
346 def enable_tracing(self):
347 """ enable tracing of hook calls and return an undo function. """
348 hooktrace = self.trace.root.get("hook")
349
350 def before(hook_name, methods, kwargs):
351 hooktrace.root.indent += 1
352 hooktrace(hook_name, kwargs)
353
354 def after(outcome, hook_name, methods, kwargs):
355 if outcome.excinfo is None:
356 hooktrace("finish", hook_name, "-->", outcome.get_result())
357 hooktrace.root.indent -= 1
358
359 return self.add_hookcall_monitoring(before, after)
360
361 def subset_hook_caller(self, name, remove_plugins):
362 """ Return a new :py:class:`.hooks._HookCaller` instance for the named method
363 which manages calls to all registered plugins except the
364 ones from remove_plugins. """
365 orig = getattr(self.hook, name)
366 plugins_to_remove = [plug for plug in remove_plugins if hasattr(plug, name)]
367 if plugins_to_remove:
368 hc = _HookCaller(
369 orig.name, orig._hookexec, orig.spec.namespace, orig.spec.opts
370 )
371 for hookimpl in orig.get_hookimpls():
372 plugin = hookimpl.plugin
373 if plugin not in plugins_to_remove:
374 hc._add_hookimpl(hookimpl)
375 # we also keep track of this hook caller so it
376 # gets properly removed on plugin unregistration
377 self._plugin2hookcallers.setdefault(plugin, []).append(hc)
378 return hc
379 return orig
380
381
382 if hasattr(inspect, "signature"):
383
384 def _formatdef(func):
385 return "%s%s" % (func.__name__, str(inspect.signature(func)))
386
387
388 else:
389
390 def _formatdef(func):
391 return "%s%s" % (
392 func.__name__,
393 inspect.formatargspec(*inspect.getargspec(func)),
394 )