Imported Upstream version 1.2.1
[platform/upstream/python-nose.git] / nose / plugins / manager.py
1 """
2 Plugin Manager
3 --------------
4
5 A plugin manager class is used to load plugins, manage the list of
6 loaded plugins, and proxy calls to those plugins.
7
8 The plugin managers provided with nose are:
9
10 :class:`PluginManager`
11     This manager doesn't implement loadPlugins, so it can only work
12     with a static list of plugins.
13
14 :class:`BuiltinPluginManager`
15     This manager loads plugins referenced in ``nose.plugins.builtin``.
16
17 :class:`EntryPointPluginManager`
18     This manager uses setuptools entrypoints to load plugins.
19
20 :class:`ExtraPluginsPluginManager`
21     This manager loads extra plugins specified with the keyword
22     `addplugins`.
23
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`.
29
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`.
35
36 Writing a plugin manager
37 ========================
38
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`).
43
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``.
49
50 """
51 import inspect
52 import logging
53 import os
54 import sys
55 from itertools import chain as iterchain
56 from warnings import warn
57 import nose.config
58 from nose.failure import Failure
59 from nose.plugins.base import IPluginInterface
60 from nose.pyversion import sort_list
61
62 try:
63     import cPickle as pickle
64 except:
65     import pickle
66 try:
67     from cStringIO import StringIO
68 except:
69     from StringIO import StringIO
70
71
72 __all__ = ['DefaultPluginManager', 'PluginManager', 'EntryPointPluginManager',
73            'BuiltinPluginManager', 'RestrictedPluginManager']
74
75 log = logging.getLogger(__name__)
76
77
78 class PluginProxy(object):
79     """Proxy for plugin calls. Essentially a closure bound to the
80     given call and plugin list.
81
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.
85     """
86     interface = IPluginInterface
87     def __init__(self, call, plugins):
88         try:
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)
94         self.plugins = []
95         for p in plugins:
96             self.addPlugin(p, call)
97
98     def __call__(self, *arg, **kw):
99         return self.call(*arg, **kw)
100
101     def addPlugin(self, plugin, call):
102         """Add plugin to my list of plugins to call, if it has the attribute
103         I'm bound to.
104         """
105         meth = getattr(plugin, call, None)
106         if meth is not None:
107             if call == 'loadTestsFromModule' and \
108                     len(inspect.getargspec(meth)[0]) == 2:
109                 orig_meth = meth
110                 meth = lambda module, path, **kwargs: orig_meth(module)
111             self.plugins.append((plugin, meth))
112
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
119
120         meth = self.method
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):
125             return self.chain
126         else:
127             # return a value from the first plugin that returns non-None
128             return self.simple
129
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.
133         """
134         result = None
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)
139                   if static]
140         for p, meth in self.plugins:
141             result = meth(*arg, **kw)
142             arg = static[:]
143             arg.append(result)
144         return result
145
146     def generate(self, *arg, **kw):
147         """Call all plugins, yielding each item in each non-None result.
148         """
149         for p, meth in self.plugins:
150             result = None
151             try:
152                 result = meth(*arg, **kw)
153                 if result is not None:
154                     for r in result:
155                         yield r
156             except (KeyboardInterrupt, SystemExit):
157                 raise
158             except:
159                 exc = sys.exc_info()
160                 yield Failure(*exc)
161                 continue
162
163     def simple(self, *arg, **kw):
164         """Call all plugins, returning the first non-None result.
165         """
166         for p, meth in self.plugins:
167             result = meth(*arg, **kw)
168             if result is not None:
169                 return result
170
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.
176         """
177         suite = []
178         for p, meth in self.plugins:
179             result = meth(names, module=module)
180             if result is not None:
181                 suite_part, names = result
182                 if suite_part:
183                     suite.extend(suite_part)
184         return suite, names
185
186
187 class NoPlugins(object):
188     """Null Plugin manager that has no plugins."""
189     interface = IPluginInterface
190     def __init__(self):
191         self._plugins = self.plugins = ()
192
193     def __iter__(self):
194         return ()
195
196     def _doNothing(self, *args, **kwds):
197         pass
198
199     def _emptyIterator(self, *args, **kwds):
200         return ()
201
202     def __getattr__(self, call):
203         method = getattr(self.interface, call)
204         if getattr(method, "generative", False):
205             return self._emptyIterator
206         else:
207             return self._doNothing
208
209     def addPlugin(self, plug):
210         raise NotImplementedError()
211
212     def addPlugins(self, plugins):
213         raise NotImplementedError()
214
215     def configure(self, options, config):
216         pass
217
218     def loadPlugins(self):
219         pass
220
221     def sort(self):
222         pass
223
224
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.
230
231     The basic functionality of a plugin manager is to proxy all unknown
232     attributes through a ``PluginProxy`` to a list of plugins.
233
234     Note that the list of plugins *may not* be changed after the first plugin
235     call.
236     """
237     proxyClass = PluginProxy
238
239     def __init__(self, plugins=(), proxyClass=None):
240         self._plugins = []
241         self._extraplugins = ()
242         self._proxies = {}
243         if plugins:
244             self.addPlugins(plugins)
245         if proxyClass is not None:
246             self.proxyClass = proxyClass
247
248     def __getattr__(self, call):
249         try:
250             return self._proxies[call]
251         except KeyError:
252             proxy = self.proxyClass(call, self._plugins)
253             self._proxies[call] = proxy
254         return proxy
255
256     def __iter__(self):
257         return iter(self.plugins)
258
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)
266
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
271         """
272         self._extraplugins = extraplugins
273         for plug in iterchain(plugins, extraplugins):
274             self.addPlugin(plug)
275
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.
280         """
281         log.debug("Configuring plugins")
282         self.config = config
283         cfg = PluginProxy('configure', self._plugins)
284         cfg(options, config)
285         enabled = [plug for plug in self._plugins if plug.enabled]
286         self.plugins = enabled
287         self.sort()
288         log.debug("Plugins enabled: %s", enabled)
289
290     def loadPlugins(self):
291         for plug in self._extraplugins:
292             self.addPlugin(plug)
293
294     def sort(self):
295         return sort_list(self._plugins, lambda x: getattr(x, 'score', 1), reverse=True)
296
297     def _get_plugins(self):
298         return self._plugins
299
300     def _set_plugins(self, plugins):
301         self._plugins = []
302         self.addPlugins(plugins)
303
304     plugins = property(_get_plugins, _set_plugins, None,
305                        """Access the list of plugins managed by
306                        this plugin manager""")
307
308
309 class ZeroNinePlugin:
310     """Proxy for 0.9 plugins, adapts 0.10 calls to 0.9 standard.
311     """
312     def __init__(self, plugin):
313         self.plugin = plugin
314
315     def options(self, parser, env=os.environ):
316         self.plugin.add_options(parser, env)
317
318     def addError(self, test, err):
319         if not hasattr(self.plugin, 'addError'):
320             return
321         # switch off to addSkip, addDeprecated if those types
322         from nose.exc import SkipTest, DeprecatedTest
323         ec, ev, tb = err
324         if issubclass(ec, SkipTest):
325             if not hasattr(self.plugin, 'addSkip'):
326                 return
327             return self.plugin.addSkip(test.test)
328         elif issubclass(ec, DeprecatedTest):
329             if not hasattr(self.plugin, 'addDeprecated'):
330                 return
331             return self.plugin.addDeprecated(test.test)
332         # add capt
333         capt = test.capturedOutput
334         return self.plugin.addError(test.test, err, capt)
335
336     def loadTestsFromFile(self, filename):
337         if hasattr(self.plugin, 'loadTestsFromPath'):
338             return self.plugin.loadTestsFromPath(filename)
339
340     def addFailure(self, test, err):
341         if not hasattr(self.plugin, 'addFailure'):
342             return
343         # add capt and tbinfo
344         capt = test.capturedOutput
345         tbinfo = test.tbinfo
346         return self.plugin.addFailure(test.test, err, capt, tbinfo)
347
348     def addSuccess(self, test):
349         if not hasattr(self.plugin, 'addSuccess'):
350             return
351         capt = test.capturedOutput
352         self.plugin.addSuccess(test.test, capt)
353
354     def startTest(self, test):
355         if not hasattr(self.plugin, 'startTest'):
356             return
357         return self.plugin.startTest(test.test)
358
359     def stopTest(self, test):
360         if not hasattr(self.plugin, 'stopTest'):
361             return
362         return self.plugin.stopTest(test.test)
363
364     def __getattr__(self, val):
365         return getattr(self.plugin, val)
366
367
368 class EntryPointPluginManager(PluginManager):
369     """Plugin manager that loads plugins from the `nose.plugins` and
370     `nose.plugins.0.10` entry points.
371     """
372     entry_points = (('nose.plugins.0.10', None),
373                     ('nose.plugins', ZeroNinePlugin))
374
375     def loadPlugins(self):
376         """Load plugins by iterating the `nose.plugins` entry point.
377         """
378         from pkg_resources import iter_entry_points
379         loaded = {}
380         for entry_point, adapt in self.entry_points:
381             for ep in iter_entry_points(entry_point):
382                 if ep.name in loaded:
383                     continue
384                 loaded[ep.name] = True
385                 log.debug('%s load plugin %s', self.__class__.__name__, ep)
386                 try:
387                     plugcls = ep.load()
388                 except KeyboardInterrupt:
389                     raise
390                 except Exception, e:
391                     # never want a plugin load to kill the test run
392                     # but we can't log here because the logger is not yet
393                     # configured
394                     warn("Unable to load plugin %s: %s" % (ep, e),
395                          RuntimeWarning)
396                     continue
397                 if adapt:
398                     plug = adapt(plugcls())
399                 else:
400                     plug = plugcls()
401                 self.addPlugin(plug)
402         super(EntryPointPluginManager, self).loadPlugins()
403
404
405 class BuiltinPluginManager(PluginManager):
406     """Plugin manager that loads plugins from the list in
407     `nose.plugins.builtin`.
408     """
409     def loadPlugins(self):
410         """Load plugins in nose.plugins.builtin
411         """
412         from nose.plugins import builtin
413         for plug in builtin.plugins:
414             self.addPlugin(plug())
415         super(BuiltinPluginManager, self).loadPlugins()
416
417 try:
418     import pkg_resources
419     class DefaultPluginManager(EntryPointPluginManager, BuiltinPluginManager):
420         pass
421
422 except ImportError:
423     class DefaultPluginManager(BuiltinPluginManager):
424         pass
425
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.
431     """
432     def __init__(self, plugins=(), exclude=(), load=True):
433         DefaultPluginManager.__init__(self, plugins)
434         self.load = load
435         self.exclude = exclude
436         self.excluded = []
437         self._excludedOpts = None
438
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)
446
447     def loadPlugins(self):
448         if self.load:
449             DefaultPluginManager.loadPlugins(self)
450         allow = []
451         for plugin in self.plugins:
452             ok = True
453             for method in self.exclude:
454                 if hasattr(plugin, method):
455                     ok = False
456                     self.excluded.append(plugin)
457                     break
458             if ok:
459                 allow.append(plugin)
460         self.plugins = allow