manager.py 15 KB

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