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