1a380bf10Simarom"""
2a380bf10SimaromPlugin Manager
3a380bf10Simarom--------------
4a380bf10Simarom
5a380bf10SimaromA plugin manager class is used to load plugins, manage the list of
6a380bf10Simaromloaded plugins, and proxy calls to those plugins.
7a380bf10Simarom
8a380bf10SimaromThe plugin managers provided with nose are:
9a380bf10Simarom
10a380bf10Simarom:class:`PluginManager`
11a380bf10Simarom    This manager doesn't implement loadPlugins, so it can only work
12a380bf10Simarom    with a static list of plugins.
13a380bf10Simarom
14a380bf10Simarom:class:`BuiltinPluginManager`
15a380bf10Simarom    This manager loads plugins referenced in ``nose.plugins.builtin``.
16a380bf10Simarom
17a380bf10Simarom:class:`EntryPointPluginManager`
18a380bf10Simarom    This manager uses setuptools entrypoints to load plugins.
19a380bf10Simarom
20a380bf10Simarom:class:`ExtraPluginsPluginManager`
21a380bf10Simarom    This manager loads extra plugins specified with the keyword
22a380bf10Simarom    `addplugins`.
23a380bf10Simarom
24a380bf10Simarom:class:`DefaultPluginMananger`
25a380bf10Simarom    This is the manager class that will be used by default. If
26a380bf10Simarom    setuptools is installed, it is a subclass of
27a380bf10Simarom    :class:`EntryPointPluginManager` and :class:`BuiltinPluginManager`;
28a380bf10Simarom    otherwise, an alias to :class:`BuiltinPluginManager`.
29a380bf10Simarom
30a380bf10Simarom:class:`RestrictedPluginManager`
31a380bf10Simarom    This manager is for use in test runs where some plugin calls are
32a380bf10Simarom    not available, such as runs started with ``python setup.py test``,
33a380bf10Simarom    where the test runner is the default unittest :class:`TextTestRunner`. It
34a380bf10Simarom    is a subclass of :class:`DefaultPluginManager`.
35a380bf10Simarom
36a380bf10SimaromWriting a plugin manager
37a380bf10Simarom========================
38a380bf10Simarom
39a380bf10SimaromIf you want to load plugins via some other means, you can write a
40a380bf10Simaromplugin manager and pass an instance of your plugin manager class when
41a380bf10Simarominstantiating the :class:`nose.config.Config` instance that you pass to
42a380bf10Simarom:class:`TestProgram` (or :func:`main` or :func:`run`).
43a380bf10Simarom
44a380bf10SimaromTo implement your plugin loading scheme, implement ``loadPlugins()``,
45a380bf10Simaromand in that method, call ``addPlugin()`` with an instance of each plugin
46a380bf10Simaromyou wish to make available. Make sure to call
47a380bf10Simarom``super(self).loadPlugins()`` as well if have subclassed a manager
48a380bf10Simaromother than ``PluginManager``.
49a380bf10Simarom
50a380bf10Simarom"""
51a380bf10Simaromimport inspect
52a380bf10Simaromimport logging
53a380bf10Simaromimport os
54a380bf10Simaromimport sys
55a380bf10Simaromfrom itertools import chain as iterchain
56a380bf10Simaromfrom warnings import warn
57a380bf10Simaromimport nose.config
58a380bf10Simaromfrom nose.failure import Failure
59a380bf10Simaromfrom nose.plugins.base import IPluginInterface
60a380bf10Simaromfrom nose.pyversion import sort_list
61a380bf10Simarom
62a380bf10Simaromtry:
63a380bf10Simarom    import pickle as pickle
64a380bf10Simaromexcept:
65a380bf10Simarom    import pickle
66a380bf10Simaromtry:
67a380bf10Simarom    from io import StringIO
68a380bf10Simaromexcept:
69a380bf10Simarom    from io import StringIO
70a380bf10Simarom
71a380bf10Simarom
72a380bf10Simarom__all__ = ['DefaultPluginManager', 'PluginManager', 'EntryPointPluginManager',
73a380bf10Simarom           'BuiltinPluginManager', 'RestrictedPluginManager']
74a380bf10Simarom
75a380bf10Simaromlog = logging.getLogger(__name__)
76a380bf10Simarom
77a380bf10Simarom
78a380bf10Simaromclass PluginProxy(object):
79a380bf10Simarom    """Proxy for plugin calls. Essentially a closure bound to the
80a380bf10Simarom    given call and plugin list.
81a380bf10Simarom
82a380bf10Simarom    The plugin proxy also must be bound to a particular plugin
83a380bf10Simarom    interface specification, so that it knows what calls are available
84a380bf10Simarom    and any special handling that is required for each call.
85a380bf10Simarom    """
86a380bf10Simarom    interface = IPluginInterface
87a380bf10Simarom    def __init__(self, call, plugins):
88a380bf10Simarom        try:
89a380bf10Simarom            self.method = getattr(self.interface, call)
90a380bf10Simarom        except AttributeError:
91a380bf10Simarom            raise AttributeError("%s is not a valid %s method"
92a380bf10Simarom                                 % (call, self.interface.__name__))
93a380bf10Simarom        self.call = self.makeCall(call)
94a380bf10Simarom        self.plugins = []
95a380bf10Simarom        for p in plugins:
96a380bf10Simarom            self.addPlugin(p, call)
97a380bf10Simarom
98a380bf10Simarom    def __call__(self, *arg, **kw):
99a380bf10Simarom        return self.call(*arg, **kw)
100a380bf10Simarom
101a380bf10Simarom    def addPlugin(self, plugin, call):
102a380bf10Simarom        """Add plugin to my list of plugins to call, if it has the attribute
103a380bf10Simarom        I'm bound to.
104a380bf10Simarom        """
105a380bf10Simarom        meth = getattr(plugin, call, None)
106a380bf10Simarom        if meth is not None:
107a380bf10Simarom            if call == 'loadTestsFromModule' and \
108a380bf10Simarom                    len(inspect.getargspec(meth)[0]) == 2:
109a380bf10Simarom                orig_meth = meth
110a380bf10Simarom                meth = lambda module, path, **kwargs: orig_meth(module)
111a380bf10Simarom            self.plugins.append((plugin, meth))
112a380bf10Simarom
113a380bf10Simarom    def makeCall(self, call):
114a380bf10Simarom        if call == 'loadTestsFromNames':
115a380bf10Simarom            # special case -- load tests from names behaves somewhat differently
116a380bf10Simarom            # from other chainable calls, because plugins return a tuple, only
117a380bf10Simarom            # part of which can be chained to the next plugin.
118a380bf10Simarom            return self._loadTestsFromNames
119a380bf10Simarom
120a380bf10Simarom        meth = self.method
121a380bf10Simarom        if getattr(meth, 'generative', False):
122a380bf10Simarom            # call all plugins and yield a flattened iterator of their results
123a380bf10Simarom            return lambda *arg, **kw: list(self.generate(*arg, **kw))
124a380bf10Simarom        elif getattr(meth, 'chainable', False):
125a380bf10Simarom            return self.chain
126a380bf10Simarom        else:
127a380bf10Simarom            # return a value from the first plugin that returns non-None
128a380bf10Simarom            return self.simple
129a380bf10Simarom
130a380bf10Simarom    def chain(self, *arg, **kw):
131a380bf10Simarom        """Call plugins in a chain, where the result of each plugin call is
132a380bf10Simarom        sent to the next plugin as input. The final output result is returned.
133a380bf10Simarom        """
134a380bf10Simarom        result = None
135a380bf10Simarom        # extract the static arguments (if any) from arg so they can
136a380bf10Simarom        # be passed to each plugin call in the chain
137a380bf10Simarom        static = [a for (static, a)
138a380bf10Simarom                  in zip(getattr(self.method, 'static_args', []), arg)
139a380bf10Simarom                  if static]
140a380bf10Simarom        for p, meth in self.plugins:
141a380bf10Simarom            result = meth(*arg, **kw)
142a380bf10Simarom            arg = static[:]
143a380bf10Simarom            arg.append(result)
144a380bf10Simarom        return result
145a380bf10Simarom
146a380bf10Simarom    def generate(self, *arg, **kw):
147a380bf10Simarom        """Call all plugins, yielding each item in each non-None result.
148a380bf10Simarom        """
149a380bf10Simarom        for p, meth in self.plugins:
150a380bf10Simarom            result = None
151a380bf10Simarom            try:
152a380bf10Simarom                result = meth(*arg, **kw)
153a380bf10Simarom                if result is not None:
154a380bf10Simarom                    for r in result:
155a380bf10Simarom                        yield r
156a380bf10Simarom            except (KeyboardInterrupt, SystemExit):
157a380bf10Simarom                raise
158a380bf10Simarom            except:
159a380bf10Simarom                exc = sys.exc_info()
160a380bf10Simarom                yield Failure(*exc)
161a380bf10Simarom                continue
162a380bf10Simarom
163a380bf10Simarom    def simple(self, *arg, **kw):
164a380bf10Simarom        """Call all plugins, returning the first non-None result.
165a380bf10Simarom        """
166a380bf10Simarom        for p, meth in self.plugins:
167a380bf10Simarom            result = meth(*arg, **kw)
168a380bf10Simarom            if result is not None:
169a380bf10Simarom                return result
170a380bf10Simarom
171a380bf10Simarom    def _loadTestsFromNames(self, names, module=None):
172a380bf10Simarom        """Chainable but not quite normal. Plugins return a tuple of
173a380bf10Simarom        (tests, names) after processing the names. The tests are added
174a380bf10Simarom        to a suite that is accumulated throughout the full call, while
175a380bf10Simarom        names are input for the next plugin in the chain.
176a380bf10Simarom        """
177a380bf10Simarom        suite = []
178a380bf10Simarom        for p, meth in self.plugins:
179a380bf10Simarom            result = meth(names, module=module)
180a380bf10Simarom            if result is not None:
181a380bf10Simarom                suite_part, names = result
182a380bf10Simarom                if suite_part:
183a380bf10Simarom                    suite.extend(suite_part)
184a380bf10Simarom        return suite, names
185a380bf10Simarom
186a380bf10Simarom
187a380bf10Simaromclass NoPlugins(object):
188a380bf10Simarom    """Null Plugin manager that has no plugins."""
189a380bf10Simarom    interface = IPluginInterface
190a380bf10Simarom    def __init__(self):
191a380bf10Simarom        self._plugins = self.plugins = ()
192a380bf10Simarom
193a380bf10Simarom    def __iter__(self):
194a380bf10Simarom        return ()
195a380bf10Simarom
196a380bf10Simarom    def _doNothing(self, *args, **kwds):
197a380bf10Simarom        pass
198a380bf10Simarom
199a380bf10Simarom    def _emptyIterator(self, *args, **kwds):
200a380bf10Simarom        return ()
201a380bf10Simarom
202a380bf10Simarom    def __getattr__(self, call):
203a380bf10Simarom        method = getattr(self.interface, call)
204a380bf10Simarom        if getattr(method, "generative", False):
205a380bf10Simarom            return self._emptyIterator
206a380bf10Simarom        else:
207a380bf10Simarom            return self._doNothing
208a380bf10Simarom
209a380bf10Simarom    def addPlugin(self, plug):
210a380bf10Simarom        raise NotImplementedError()
211a380bf10Simarom
212a380bf10Simarom    def addPlugins(self, plugins):
213a380bf10Simarom        raise NotImplementedError()
214a380bf10Simarom
215a380bf10Simarom    def configure(self, options, config):
216a380bf10Simarom        pass
217a380bf10Simarom
218a380bf10Simarom    def loadPlugins(self):
219a380bf10Simarom        pass
220a380bf10Simarom
221a380bf10Simarom    def sort(self):
222a380bf10Simarom        pass
223a380bf10Simarom
224a380bf10Simarom
225a380bf10Simaromclass PluginManager(object):
226a380bf10Simarom    """Base class for plugin managers. PluginManager is intended to be
227a380bf10Simarom    used only with a static list of plugins. The loadPlugins() implementation
228a380bf10Simarom    only reloads plugins from _extraplugins to prevent those from being
229a380bf10Simarom    overridden by a subclass.
230a380bf10Simarom
231a380bf10Simarom    The basic functionality of a plugin manager is to proxy all unknown
232a380bf10Simarom    attributes through a ``PluginProxy`` to a list of plugins.
233a380bf10Simarom
234a380bf10Simarom    Note that the list of plugins *may not* be changed after the first plugin
235a380bf10Simarom    call.
236a380bf10Simarom    """
237a380bf10Simarom    proxyClass = PluginProxy
238a380bf10Simarom
239a380bf10Simarom    def __init__(self, plugins=(), proxyClass=None):
240a380bf10Simarom        self._plugins = []
241a380bf10Simarom        self._extraplugins = ()
242a380bf10Simarom        self._proxies = {}
243a380bf10Simarom        if plugins:
244a380bf10Simarom            self.addPlugins(plugins)
245a380bf10Simarom        if proxyClass is not None:
246a380bf10Simarom            self.proxyClass = proxyClass
247a380bf10Simarom
248a380bf10Simarom    def __getattr__(self, call):
249a380bf10Simarom        try:
250a380bf10Simarom            return self._proxies[call]
251a380bf10Simarom        except KeyError:
252a380bf10Simarom            proxy = self.proxyClass(call, self._plugins)
253a380bf10Simarom            self._proxies[call] = proxy
254a380bf10Simarom        return proxy
255a380bf10Simarom
256a380bf10Simarom    def __iter__(self):
257a380bf10Simarom        return iter(self.plugins)
258a380bf10Simarom
259a380bf10Simarom    def addPlugin(self, plug):
260a380bf10Simarom        # allow, for instance, plugins loaded via entry points to
261a380bf10Simarom        # supplant builtin plugins.
262a380bf10Simarom        new_name = getattr(plug, 'name', object())
263a380bf10Simarom        self._plugins[:] = [p for p in self._plugins
264a380bf10Simarom                            if getattr(p, 'name', None) != new_name]
265a380bf10Simarom        self._plugins.append(plug)
266a380bf10Simarom
267a380bf10Simarom    def addPlugins(self, plugins=(), extraplugins=()):
268a380bf10Simarom        """extraplugins are maintained in a separate list and
269a380bf10Simarom        re-added by loadPlugins() to prevent their being overwritten
270a380bf10Simarom        by plugins added by a subclass of PluginManager
271a380bf10Simarom        """
272a380bf10Simarom        self._extraplugins = extraplugins
273a380bf10Simarom        for plug in iterchain(plugins, extraplugins):
274a380bf10Simarom            self.addPlugin(plug)
275a380bf10Simarom
276a380bf10Simarom    def configure(self, options, config):
277a380bf10Simarom        """Configure the set of plugins with the given options
278a380bf10Simarom        and config instance. After configuration, disabled plugins
279a380bf10Simarom        are removed from the plugins list.
280a380bf10Simarom        """
281a380bf10Simarom        log.debug("Configuring plugins")
282a380bf10Simarom        self.config = config
283a380bf10Simarom        cfg = PluginProxy('configure', self._plugins)
284a380bf10Simarom        cfg(options, config)
285a380bf10Simarom        enabled = [plug for plug in self._plugins if plug.enabled]
286a380bf10Simarom        self.plugins = enabled
287a380bf10Simarom        self.sort()
288a380bf10Simarom        log.debug("Plugins enabled: %s", enabled)
289a380bf10Simarom
290a380bf10Simarom    def loadPlugins(self):
291a380bf10Simarom        for plug in self._extraplugins:
292a380bf10Simarom            self.addPlugin(plug)
293a380bf10Simarom
294a380bf10Simarom    def sort(self):
295a380bf10Simarom        return sort_list(self._plugins, lambda x: getattr(x, 'score', 1), reverse=True)
296a380bf10Simarom
297a380bf10Simarom    def _get_plugins(self):
298a380bf10Simarom        return self._plugins
299a380bf10Simarom
300a380bf10Simarom    def _set_plugins(self, plugins):
301a380bf10Simarom        self._plugins = []
302a380bf10Simarom        self.addPlugins(plugins)
303a380bf10Simarom
304a380bf10Simarom    plugins = property(_get_plugins, _set_plugins, None,
305a380bf10Simarom                       """Access the list of plugins managed by
306a380bf10Simarom                       this plugin manager""")
307a380bf10Simarom
308a380bf10Simarom
309a380bf10Simaromclass ZeroNinePlugin:
310a380bf10Simarom    """Proxy for 0.9 plugins, adapts 0.10 calls to 0.9 standard.
311a380bf10Simarom    """
312a380bf10Simarom    def __init__(self, plugin):
313a380bf10Simarom        self.plugin = plugin
314a380bf10Simarom
315a380bf10Simarom    def options(self, parser, env=os.environ):
316a380bf10Simarom        self.plugin.add_options(parser, env)
317a380bf10Simarom
318a380bf10Simarom    def addError(self, test, err):
319a380bf10Simarom        if not hasattr(self.plugin, 'addError'):
320a380bf10Simarom            return
321a380bf10Simarom        # switch off to addSkip, addDeprecated if those types
322a380bf10Simarom        from nose.exc import SkipTest, DeprecatedTest
323a380bf10Simarom        ec, ev, tb = err
324a380bf10Simarom        if issubclass(ec, SkipTest):
325a380bf10Simarom            if not hasattr(self.plugin, 'addSkip'):
326a380bf10Simarom                return
327a380bf10Simarom            return self.plugin.addSkip(test.test)
328a380bf10Simarom        elif issubclass(ec, DeprecatedTest):
329a380bf10Simarom            if not hasattr(self.plugin, 'addDeprecated'):
330a380bf10Simarom                return
331a380bf10Simarom            return self.plugin.addDeprecated(test.test)
332a380bf10Simarom        # add capt
333a380bf10Simarom        capt = test.capturedOutput
334a380bf10Simarom        return self.plugin.addError(test.test, err, capt)
335a380bf10Simarom
336a380bf10Simarom    def loadTestsFromFile(self, filename):
337a380bf10Simarom        if hasattr(self.plugin, 'loadTestsFromPath'):
338a380bf10Simarom            return self.plugin.loadTestsFromPath(filename)
339a380bf10Simarom
340a380bf10Simarom    def addFailure(self, test, err):
341a380bf10Simarom        if not hasattr(self.plugin, 'addFailure'):
342a380bf10Simarom            return
343a380bf10Simarom        # add capt and tbinfo
344a380bf10Simarom        capt = test.capturedOutput
345a380bf10Simarom        tbinfo = test.tbinfo
346a380bf10Simarom        return self.plugin.addFailure(test.test, err, capt, tbinfo)
347a380bf10Simarom
348a380bf10Simarom    def addSuccess(self, test):
349a380bf10Simarom        if not hasattr(self.plugin, 'addSuccess'):
350a380bf10Simarom            return
351a380bf10Simarom        capt = test.capturedOutput
352a380bf10Simarom        self.plugin.addSuccess(test.test, capt)
353a380bf10Simarom
354a380bf10Simarom    def startTest(self, test):
355a380bf10Simarom        if not hasattr(self.plugin, 'startTest'):
356a380bf10Simarom            return
357a380bf10Simarom        return self.plugin.startTest(test.test)
358a380bf10Simarom
359a380bf10Simarom    def stopTest(self, test):
360a380bf10Simarom        if not hasattr(self.plugin, 'stopTest'):
361a380bf10Simarom            return
362a380bf10Simarom        return self.plugin.stopTest(test.test)
363a380bf10Simarom
364a380bf10Simarom    def __getattr__(self, val):
365a380bf10Simarom        return getattr(self.plugin, val)
366a380bf10Simarom
367a380bf10Simarom
368a380bf10Simaromclass EntryPointPluginManager(PluginManager):
369a380bf10Simarom    """Plugin manager that loads plugins from the `nose.plugins` and
370a380bf10Simarom    `nose.plugins.0.10` entry points.
371a380bf10Simarom    """
372a380bf10Simarom    entry_points = (('nose.plugins.0.10', None),
373a380bf10Simarom                    ('nose.plugins', ZeroNinePlugin))
374a380bf10Simarom
375a380bf10Simarom    def loadPlugins(self):
376a380bf10Simarom        """Load plugins by iterating the `nose.plugins` entry point.
377a380bf10Simarom        """
378a380bf10Simarom        from pkg_resources import iter_entry_points
379a380bf10Simarom        loaded = {}
380a380bf10Simarom        for entry_point, adapt in self.entry_points:
381a380bf10Simarom            for ep in iter_entry_points(entry_point):
382a380bf10Simarom                if ep.name in loaded:
383a380bf10Simarom                    continue
384a380bf10Simarom                loaded[ep.name] = True
385a380bf10Simarom                log.debug('%s load plugin %s', self.__class__.__name__, ep)
386a380bf10Simarom                try:
387a380bf10Simarom                    plugcls = ep.load()
388a380bf10Simarom                except KeyboardInterrupt:
389a380bf10Simarom                    raise
390a380bf10Simarom                except Exception as e:
391a380bf10Simarom                    # never want a plugin load to kill the test run
392a380bf10Simarom                    # but we can't log here because the logger is not yet
393a380bf10Simarom                    # configured
394a380bf10Simarom                    warn("Unable to load plugin %s: %s" % (ep, e),
395a380bf10Simarom                         RuntimeWarning)
396a380bf10Simarom                    continue
397a380bf10Simarom                if adapt:
398a380bf10Simarom                    plug = adapt(plugcls())
399a380bf10Simarom                else:
400a380bf10Simarom                    plug = plugcls()
401a380bf10Simarom                self.addPlugin(plug)
402a380bf10Simarom        super(EntryPointPluginManager, self).loadPlugins()
403a380bf10Simarom
404a380bf10Simarom
405a380bf10Simaromclass BuiltinPluginManager(PluginManager):
406a380bf10Simarom    """Plugin manager that loads plugins from the list in
407a380bf10Simarom    `nose.plugins.builtin`.
408a380bf10Simarom    """
409a380bf10Simarom    def loadPlugins(self):
410a380bf10Simarom        """Load plugins in nose.plugins.builtin
411a380bf10Simarom        """
412a380bf10Simarom        from nose.plugins import builtin
413a380bf10Simarom        for plug in builtin.plugins:
414a380bf10Simarom            self.addPlugin(plug())
415a380bf10Simarom        super(BuiltinPluginManager, self).loadPlugins()
416a380bf10Simarom
417a380bf10Simaromtry:
418a380bf10Simarom    import pkg_resources
419a380bf10Simarom    class DefaultPluginManager(EntryPointPluginManager, BuiltinPluginManager):
420a380bf10Simarom        pass
421a380bf10Simarom
422a380bf10Simaromexcept ImportError:
423a380bf10Simarom    class DefaultPluginManager(BuiltinPluginManager):
424a380bf10Simarom        pass
425a380bf10Simarom
426a380bf10Simaromclass RestrictedPluginManager(DefaultPluginManager):
427a380bf10Simarom    """Plugin manager that restricts the plugin list to those not
428a380bf10Simarom    excluded by a list of exclude methods. Any plugin that implements
429a380bf10Simarom    an excluded method will be removed from the manager's plugin list
430a380bf10Simarom    after plugins are loaded.
431a380bf10Simarom    """
432a380bf10Simarom    def __init__(self, plugins=(), exclude=(), load=True):
433a380bf10Simarom        DefaultPluginManager.__init__(self, plugins)
434a380bf10Simarom        self.load = load
435a380bf10Simarom        self.exclude = exclude
436a380bf10Simarom        self.excluded = []
437a380bf10Simarom        self._excludedOpts = None
438a380bf10Simarom
439a380bf10Simarom    def excludedOption(self, name):
440a380bf10Simarom        if self._excludedOpts is None:
441a380bf10Simarom            from optparse import OptionParser
442a380bf10Simarom            self._excludedOpts = OptionParser(add_help_option=False)
443a380bf10Simarom            for plugin in self.excluded:
444a380bf10Simarom                plugin.options(self._excludedOpts, env={})
445a380bf10Simarom        return self._excludedOpts.get_option('--' + name)
446a380bf10Simarom
447a380bf10Simarom    def loadPlugins(self):
448a380bf10Simarom        if self.load:
449a380bf10Simarom            DefaultPluginManager.loadPlugins(self)
450a380bf10Simarom        allow = []
451a380bf10Simarom        for plugin in self.plugins:
452a380bf10Simarom            ok = True
453a380bf10Simarom            for method in self.exclude:
454a380bf10Simarom                if hasattr(plugin, method):
455a380bf10Simarom                    ok = False
456a380bf10Simarom                    self.excluded.append(plugin)
457a380bf10Simarom                    break
458a380bf10Simarom            if ok:
459a380bf10Simarom                allow.append(plugin)
460a380bf10Simarom        self.plugins = allow
461