1"""
2Plugin Manager
3--------------
4
5A plugin manager class is used to load plugins, manage the list of
6loaded plugins, and proxy calls to those plugins.
7
8The 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
36Writing a plugin manager
37========================
38
39If you want to load plugins via some other means, you can write a
40plugin manager and pass an instance of your plugin manager class when
41instantiating the :class:`nose.config.Config` instance that you pass to
42:class:`TestProgram` (or :func:`main` or :func:`run`).
43
44To implement your plugin loading scheme, implement ``loadPlugins()``,
45and in that method, call ``addPlugin()`` with an instance of each plugin
46you wish to make available. Make sure to call
47``super(self).loadPlugins()`` as well if have subclassed a manager
48other than ``PluginManager``.
49
50"""
51import inspect
52import logging
53import os
54import sys
55from itertools import chain as iterchain
56from warnings import warn
57import nose.config
58from nose.failure import Failure
59from nose.plugins.base import IPluginInterface
60from nose.pyversion import sort_list
61
62try:
63    import cPickle as pickle
64except:
65    import pickle
66try:
67    from cStringIO import StringIO
68except:
69    from StringIO import StringIO
70
71
72__all__ = ['DefaultPluginManager', 'PluginManager', 'EntryPointPluginManager',
73           'BuiltinPluginManager', 'RestrictedPluginManager']
74
75log = logging.getLogger(__name__)
76
77
78class 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
187class 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
225class 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
309class 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
368class 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
405class 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
417try:
418    import pkg_resources
419    class DefaultPluginManager(EntryPointPluginManager, BuiltinPluginManager):
420        pass
421
422except ImportError:
423    class DefaultPluginManager(BuiltinPluginManager):
424        pass
425
426class 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
461