5 A plugin manager class is used to load plugins, manage the list of
6 loaded plugins, and proxy calls to those plugins.
8 The plugin managers provided with nose are:
10 :class:`PluginManager`
11 This manager doesn't implement loadPlugins, so it can only work
12 with a static list of plugins.
14 :class:`BuiltinPluginManager`
15 This manager loads plugins referenced in ``nose.plugins.builtin``.
17 :class:`EntryPointPluginManager`
18 This manager uses setuptools entrypoints to load plugins.
20 :class:`ExtraPluginsPluginManager`
21 This manager loads extra plugins specified with the keyword
24 :class:`DefaultPluginMananger`
25 This is the manager class that will be used by default. If
26 setuptools is installed, it is a subclass of
27 :class:`EntryPointPluginManager` and :class:`BuiltinPluginManager`;
28 otherwise, an alias to :class:`BuiltinPluginManager`.
30 :class:`RestrictedPluginManager`
31 This manager is for use in test runs where some plugin calls are
32 not available, such as runs started with ``python setup.py test``,
33 where the test runner is the default unittest :class:`TextTestRunner`. It
34 is a subclass of :class:`DefaultPluginManager`.
36 Writing a plugin manager
37 ========================
39 If you want to load plugins via some other means, you can write a
40 plugin manager and pass an instance of your plugin manager class when
41 instantiating the :class:`nose.config.Config` instance that you pass to
42 :class:`TestProgram` (or :func:`main` or :func:`run`).
44 To implement your plugin loading scheme, implement ``loadPlugins()``,
45 and in that method, call ``addPlugin()`` with an instance of each plugin
46 you wish to make available. Make sure to call
47 ``super(self).loadPlugins()`` as well if have subclassed a manager
48 other than ``PluginManager``.
55 from itertools import chain as iterchain
56 from warnings import warn
58 from nose.failure import Failure
59 from nose.plugins.base import IPluginInterface
60 from nose.pyversion import sort_list
63 import cPickle as pickle
67 from cStringIO import StringIO
69 from StringIO import StringIO
72 __all__ = ['DefaultPluginManager', 'PluginManager', 'EntryPointPluginManager',
73 'BuiltinPluginManager', 'RestrictedPluginManager']
75 log = logging.getLogger(__name__)
78 class PluginProxy(object):
79 """Proxy for plugin calls. Essentially a closure bound to the
80 given call and plugin list.
82 The plugin proxy also must be bound to a particular plugin
83 interface specification, so that it knows what calls are available
84 and any special handling that is required for each call.
86 interface = IPluginInterface
87 def __init__(self, call, plugins):
89 self.method = getattr(self.interface, call)
90 except AttributeError:
91 raise AttributeError("%s is not a valid %s method"
92 % (call, self.interface.__name__))
93 self.call = self.makeCall(call)
96 self.addPlugin(p, call)
98 def __call__(self, *arg, **kw):
99 return self.call(*arg, **kw)
101 def addPlugin(self, plugin, call):
102 """Add plugin to my list of plugins to call, if it has the attribute
105 meth = getattr(plugin, call, None)
107 if call == 'loadTestsFromModule' and \
108 len(inspect.getargspec(meth)[0]) == 2:
110 meth = lambda module, path, **kwargs: orig_meth(module)
111 self.plugins.append((plugin, meth))
113 def makeCall(self, call):
114 if call == 'loadTestsFromNames':
115 # special case -- load tests from names behaves somewhat differently
116 # from other chainable calls, because plugins return a tuple, only
117 # part of which can be chained to the next plugin.
118 return self._loadTestsFromNames
121 if getattr(meth, 'generative', False):
122 # call all plugins and yield a flattened iterator of their results
123 return lambda *arg, **kw: list(self.generate(*arg, **kw))
124 elif getattr(meth, 'chainable', False):
127 # return a value from the first plugin that returns non-None
130 def chain(self, *arg, **kw):
131 """Call plugins in a chain, where the result of each plugin call is
132 sent to the next plugin as input. The final output result is returned.
135 # extract the static arguments (if any) from arg so they can
136 # be passed to each plugin call in the chain
137 static = [a for (static, a)
138 in zip(getattr(self.method, 'static_args', []), arg)
140 for p, meth in self.plugins:
141 result = meth(*arg, **kw)
146 def generate(self, *arg, **kw):
147 """Call all plugins, yielding each item in each non-None result.
149 for p, meth in self.plugins:
152 result = meth(*arg, **kw)
153 if result is not None:
156 except (KeyboardInterrupt, SystemExit):
163 def simple(self, *arg, **kw):
164 """Call all plugins, returning the first non-None result.
166 for p, meth in self.plugins:
167 result = meth(*arg, **kw)
168 if result is not None:
171 def _loadTestsFromNames(self, names, module=None):
172 """Chainable but not quite normal. Plugins return a tuple of
173 (tests, names) after processing the names. The tests are added
174 to a suite that is accumulated throughout the full call, while
175 names are input for the next plugin in the chain.
178 for p, meth in self.plugins:
179 result = meth(names, module=module)
180 if result is not None:
181 suite_part, names = result
183 suite.extend(suite_part)
187 class NoPlugins(object):
188 """Null Plugin manager that has no plugins."""
189 interface = IPluginInterface
191 self._plugins = self.plugins = ()
196 def _doNothing(self, *args, **kwds):
199 def _emptyIterator(self, *args, **kwds):
202 def __getattr__(self, call):
203 method = getattr(self.interface, call)
204 if getattr(method, "generative", False):
205 return self._emptyIterator
207 return self._doNothing
209 def addPlugin(self, plug):
210 raise NotImplementedError()
212 def addPlugins(self, plugins):
213 raise NotImplementedError()
215 def configure(self, options, config):
218 def loadPlugins(self):
225 class PluginManager(object):
226 """Base class for plugin managers. PluginManager is intended to be
227 used only with a static list of plugins. The loadPlugins() implementation
228 only reloads plugins from _extraplugins to prevent those from being
229 overridden by a subclass.
231 The basic functionality of a plugin manager is to proxy all unknown
232 attributes through a ``PluginProxy`` to a list of plugins.
234 Note that the list of plugins *may not* be changed after the first plugin
237 proxyClass = PluginProxy
239 def __init__(self, plugins=(), proxyClass=None):
241 self._extraplugins = ()
244 self.addPlugins(plugins)
245 if proxyClass is not None:
246 self.proxyClass = proxyClass
248 def __getattr__(self, call):
250 return self._proxies[call]
252 proxy = self.proxyClass(call, self._plugins)
253 self._proxies[call] = proxy
257 return iter(self.plugins)
259 def addPlugin(self, plug):
260 # allow, for instance, plugins loaded via entry points to
261 # supplant builtin plugins.
262 new_name = getattr(plug, 'name', object())
263 self._plugins[:] = [p for p in self._plugins
264 if getattr(p, 'name', None) != new_name]
265 self._plugins.append(plug)
267 def addPlugins(self, plugins=(), extraplugins=()):
268 """extraplugins are maintained in a separate list and
269 re-added by loadPlugins() to prevent their being overwritten
270 by plugins added by a subclass of PluginManager
272 self._extraplugins = extraplugins
273 for plug in iterchain(plugins, extraplugins):
276 def configure(self, options, config):
277 """Configure the set of plugins with the given options
278 and config instance. After configuration, disabled plugins
279 are removed from the plugins list.
281 log.debug("Configuring plugins")
283 cfg = PluginProxy('configure', self._plugins)
285 enabled = [plug for plug in self._plugins if plug.enabled]
286 self.plugins = enabled
288 log.debug("Plugins enabled: %s", enabled)
290 def loadPlugins(self):
291 for plug in self._extraplugins:
295 return sort_list(self._plugins, lambda x: getattr(x, 'score', 1), reverse=True)
297 def _get_plugins(self):
300 def _set_plugins(self, plugins):
302 self.addPlugins(plugins)
304 plugins = property(_get_plugins, _set_plugins, None,
305 """Access the list of plugins managed by
306 this plugin manager""")
309 class ZeroNinePlugin:
310 """Proxy for 0.9 plugins, adapts 0.10 calls to 0.9 standard.
312 def __init__(self, plugin):
315 def options(self, parser, env=os.environ):
316 self.plugin.add_options(parser, env)
318 def addError(self, test, err):
319 if not hasattr(self.plugin, 'addError'):
321 # switch off to addSkip, addDeprecated if those types
322 from nose.exc import SkipTest, DeprecatedTest
324 if issubclass(ec, SkipTest):
325 if not hasattr(self.plugin, 'addSkip'):
327 return self.plugin.addSkip(test.test)
328 elif issubclass(ec, DeprecatedTest):
329 if not hasattr(self.plugin, 'addDeprecated'):
331 return self.plugin.addDeprecated(test.test)
333 capt = test.capturedOutput
334 return self.plugin.addError(test.test, err, capt)
336 def loadTestsFromFile(self, filename):
337 if hasattr(self.plugin, 'loadTestsFromPath'):
338 return self.plugin.loadTestsFromPath(filename)
340 def addFailure(self, test, err):
341 if not hasattr(self.plugin, 'addFailure'):
343 # add capt and tbinfo
344 capt = test.capturedOutput
346 return self.plugin.addFailure(test.test, err, capt, tbinfo)
348 def addSuccess(self, test):
349 if not hasattr(self.plugin, 'addSuccess'):
351 capt = test.capturedOutput
352 self.plugin.addSuccess(test.test, capt)
354 def startTest(self, test):
355 if not hasattr(self.plugin, 'startTest'):
357 return self.plugin.startTest(test.test)
359 def stopTest(self, test):
360 if not hasattr(self.plugin, 'stopTest'):
362 return self.plugin.stopTest(test.test)
364 def __getattr__(self, val):
365 return getattr(self.plugin, val)
368 class EntryPointPluginManager(PluginManager):
369 """Plugin manager that loads plugins from the `nose.plugins` and
370 `nose.plugins.0.10` entry points.
372 entry_points = (('nose.plugins.0.10', None),
373 ('nose.plugins', ZeroNinePlugin))
375 def loadPlugins(self):
376 """Load plugins by iterating the `nose.plugins` entry point.
378 from pkg_resources import iter_entry_points
380 for entry_point, adapt in self.entry_points:
381 for ep in iter_entry_points(entry_point):
382 if ep.name in loaded:
384 loaded[ep.name] = True
385 log.debug('%s load plugin %s', self.__class__.__name__, ep)
388 except KeyboardInterrupt:
391 # never want a plugin load to kill the test run
392 # but we can't log here because the logger is not yet
394 warn("Unable to load plugin %s: %s" % (ep, e),
398 plug = adapt(plugcls())
402 super(EntryPointPluginManager, self).loadPlugins()
405 class BuiltinPluginManager(PluginManager):
406 """Plugin manager that loads plugins from the list in
407 `nose.plugins.builtin`.
409 def loadPlugins(self):
410 """Load plugins in nose.plugins.builtin
412 from nose.plugins import builtin
413 for plug in builtin.plugins:
414 self.addPlugin(plug())
415 super(BuiltinPluginManager, self).loadPlugins()
419 class DefaultPluginManager(EntryPointPluginManager, BuiltinPluginManager):
423 class DefaultPluginManager(BuiltinPluginManager):
426 class RestrictedPluginManager(DefaultPluginManager):
427 """Plugin manager that restricts the plugin list to those not
428 excluded by a list of exclude methods. Any plugin that implements
429 an excluded method will be removed from the manager's plugin list
430 after plugins are loaded.
432 def __init__(self, plugins=(), exclude=(), load=True):
433 DefaultPluginManager.__init__(self, plugins)
435 self.exclude = exclude
437 self._excludedOpts = None
439 def excludedOption(self, name):
440 if self._excludedOpts is None:
441 from optparse import OptionParser
442 self._excludedOpts = OptionParser(add_help_option=False)
443 for plugin in self.excluded:
444 plugin.options(self._excludedOpts, env={})
445 return self._excludedOpts.get_option('--' + name)
447 def loadPlugins(self):
449 DefaultPluginManager.loadPlugins(self)
451 for plugin in self.plugins:
453 for method in self.exclude:
454 if hasattr(plugin, method):
456 self.excluded.append(plugin)