nosetests源码解析 插件实现设计模式--调色板模式

    背景:最近需要扩展一下nosetest框架插件,项目团队已有自造多个插件而且效果还不错。燃鹅,并没有去深究源码和插件调用和设计实现。于是,下断点

跟踪nosetests框架源码。开局一只狗,断断续续花了近一周时间才还原出插件的调用逻辑,惊呼大神的脑洞清奇,设计模式方面完全不拘一格。基于插件的

实现模式,还原类比出这一复合设计模式--调色板模式。

I.隐喻

    按照code complete范式, 软件所描的对象需要抽象和隐喻. 抽象对象越具体, 隐喻类比的越等价,则代码越是能够自解释. 这个理论对复杂的设计模式更有效.

1.1 调色板模型构造

                                              图1-1 调色板形态

    有一位天才画家喜欢用4X4的调色板如图左, 每个各自可以容纳一种颜色. 调好颜料后, 调色板的形态如图右, 每个格子一种颜色或者为空(没有颜色). 但是, 画家

进入了创作瓶经期, 已经很久自己调制出一组满意的色彩了. 于是他设计了一种随机调色板, 通过他人随机涂鸦组合出富有创造性的颜色:

    a. 准备一盒彩笔. 制作一批无色透明的塑料卡片(如图1-1), 每一张用按照图1-1划分成16格;

    b. 随机找一位陌生人, 给他一张做好的塑料卡片, 并请他用彩笔给格子填颜色. 可以选任意颜色, 可以给每个格子涂一种颜色或者留白.

    c. 多次执行b步骤, 汇总得到的卡片包备用.

    d. 将卡片包整理, 凭个人喜好去除不喜欢的卡片, 并按照某种顺序排列.

    e. 如下图, 将卡片按照顺序沿箭头方向排列, 固定在架子上. 

    f. 在最后一层卡片后合适的位置放置白色的屏幕. 在卡片的顶部放置自然光源, 使得光线平行均匀的按照箭头方向穿过一层层卡片, 最终在幕布上形成调色板.

    g. 记录下调色板的颜色.

                                                                                 图1-2 调色板结构

调色板模式的特点:

    * a.扩展性强: 图层的结构简单, 理论上可以无限扩展. 屏幕上生成的颜色板等于各图层相应格子的光学叠加.

    * b.内聚性强: 图层的构造受限于彩笔颜色集合和简单的填涂规则(分格单色填充), 其他全部交由涂色第三方处理. 产生屏幕上调色板的过程插件一概不知, 只知道

         对自己感兴趣的格子做了填涂. 

 

 II. nose插件设计模式

    nosetests框架的强大之处正是易于扩展的插件和屏蔽得非常完美的内部实现. 客户层(开发者)只需要依照极少数规则继承插件基类, 实现&重载自己感兴趣的模块就

可以了, 丝毫不用关心内部实现和调用细节. 理论上可以无线扩展. 对照调色板模式的隐喻, nosetests插件模式的映射为:

* 调色板分格原型:  nose.plugin.base基类, nose插件的基础模型. 其中Plugin类是插件基类, 所有插件需要从该类继承. IPluginInterface类是一个协议类, 依字母顺序

          枚举了nose执行过程中可以扩展的点,  都是执行过程的前置后置等场景. 有点AOP(面向切片编程)的意思. 一个插件需要继承Plugin但不允许继承

          IPluginInterface, 而又可以实现IPluginInterface的方法. 这个有点意思! 类比于调色板的分格原型, IPluginInterface将插件可以扩展的点罗列出来,

                               每个点对应执行过程中的某个步骤, 相当与调色板的格子. 

* 调色板透明卡片:  具体的nose插件, 如nose.plugin.attrib:AttributeSelector类. 它继承自nose.plugin.base, 同时又实现了wantedMethod wantedFunction函数. 用来

                               过滤出需要执行的测试用例. attrib插件仅仅实现了测试用例筛选过程, 其他的"格子"全部留白. 相当于只给"1个格子"涂色, 其他"15个格子"留白的

                               透明卡片. 光线经过这个卡片,只有这个格子的颜色发生改变.

* 调色板光合成器:  nose.plugin.manager模块, 横向--他驱动光线在平面上移动, 访问没一个格子, 纵向--他驱动光线透过层层卡片最终在屏幕上合成调色板.

      * 栅格扫描器:  nose.plugin.manager:PluginManager及其子类一起扫描所有插件并存放到该类, 通过运行参数和环境变量过滤出需要的插件. 

      * 透光合成器:  nose.plugin.manager:PluginProxy实现类透光合成步骤, 合成单个格子最终的颜色.

III. nose插件代码概览

    nose插件的实现主要使用的是代理模式, 而且用得非常的pythonical!

nose.plugin.base.py模块源码

  1 import os
  2 import textwrap
  3 from optparse import OptionConflictError
  4 from warnings import warn
  5 from nose.util import tolist
  6 
  7 class Plugin(object):
  8     """Base class for nose plugins. It's recommended but not *necessary* to
  9     subclass this class to create a plugin, but all plugins *must* implement
 10     `options(self, parser, env)` and `configure(self, options, conf)`, and
 11     must have the attributes `enabled`, `name` and `score`.  The `name`
 12     attribute may contain hyphens ('-').
 13     Plugins should not be enabled by default.
 14     Subclassing Plugin (and calling the superclass methods in
 15     __init__, configure, and options, if you override them) will give
 16     your plugin some friendly default behavior:
 17     * A --with-$name option will be added to the command line interface
 18       to enable the plugin, and a corresponding environment variable
 19       will be used as the default value. The plugin class's docstring
 20       will be used as the help for this option.
 21     * The plugin will not be enabled unless this option is selected by
 22       the user.
 23     """
 24     can_configure = False
 25     enabled = False
 26     enableOpt = None
 27     name = None
 28     score = 100
 29 
 30     def __init__(self):
 31         if self.name is None:
 32             self.name = self.__class__.__name__.lower()
 33         if self.enableOpt is None:
 34             self.enableOpt = "enable_plugin_%s" % self.name.replace('-', '_')
 35 
 36     def addOptions(self, parser, env=None):
 37         """Add command-line options for this plugin.
 38         The base plugin class adds --with-$name by default, used to enable the
 39         plugin.
 40         .. warning :: Don't implement addOptions unless you want to override
 41                       all default option handling behavior, including
 42                       warnings for conflicting options. Implement
 43                       :meth:`options
 44                       <nose.plugins.base.IPluginInterface.options>`
 45                       instead.
 46         """
 47         self.add_options(parser, env)
 48 
 49     def add_options(self, parser, env=None):
 50         """Non-camel-case version of func name for backwards compatibility.
 51         .. warning ::
 52            DEPRECATED: Do not use this method,
 53            use :meth:`options <nose.plugins.base.IPluginInterface.options>`
 54            instead.
 55         """
 56         # FIXME raise deprecation warning if wasn't called by wrapper
 57         if env is None:
 58             env = os.environ
 59         try:
 60             self.options(parser, env)
 61             self.can_configure = True
 62         except OptionConflictError, e:
 63             warn("Plugin %s has conflicting option string: %s and will "
 64                  "be disabled" % (self, e), RuntimeWarning)
 65             self.enabled = False
 66             self.can_configure = False
 67 
 68     def options(self, parser, env):
 69         """Register commandline options.
 70         Implement this method for normal options behavior with protection from
 71         OptionConflictErrors. If you override this method and want the default
 72         --with-$name option to be registered, be sure to call super().
 73         """
 74         env_opt = 'NOSE_WITH_%s' % self.name.upper()
 75         env_opt = env_opt.replace('-', '_')
 76         parser.add_option("--with-%s" % self.name,
 77                           action="store_true",
 78                           dest=self.enableOpt,
 79                           default=env.get(env_opt),
 80                           help="Enable plugin %s: %s [%s]" %
 81                           (self.__class__.__name__, self.help(), env_opt))
 82 
 83     def configure(self, options, conf):
 84         """Configure the plugin and system, based on selected options.
 85         The base plugin class sets the plugin to enabled if the enable option
 86         for the plugin (self.enableOpt) is true.
 87         """
 88         if not self.can_configure:
 89             return
 90         self.conf = conf
 91         if hasattr(options, self.enableOpt):
 92             self.enabled = getattr(options, self.enableOpt)
 93 
 94     def help(self):
 95         """Return help for this plugin. This will be output as the help
 96         section of the --with-$name option that enables the plugin.
 97         """
 98         if self.__class__.__doc__:
 99             # doc sections are often indented; compress the spaces
100             return textwrap.dedent(self.__class__.__doc__)
101         return "(no help available)"
102 
103     # Compatiblity shim
104     def tolist(self, val):
105         warn("Plugin.tolist is deprecated. Use nose.util.tolist instead",
106              DeprecationWarning)
107         return tolist(val)
108 
109 
110 class IPluginInterface(object):
111     """
112     IPluginInterface describes the plugin API. Do not subclass or use this
113     class directly.
114     """
115     def __new__(cls, *arg, **kw):
116         raise TypeError("IPluginInterface class is for documentation only")
117 
118     def addOptions(self, parser, env):
119         """Called to allow plugin to register command-line options with the
120         parser. DO NOT return a value from this method unless you want to stop
121         all other plugins from setting their options.
122         .. warning ::
123            DEPRECATED -- implement
124            :meth:`options <nose.plugins.base.IPluginInterface.options>` instead.
125         """
126         pass
127     add_options = addOptions
128     add_options.deprecated = True
129 
130     def addDeprecated(self, test):
131         """Called when a deprecated test is seen. DO NOT return a value
132         unless you want to stop other plugins from seeing the deprecated
133         test.
134         .. warning :: DEPRECATED -- check error class in addError instead
135         """
136         pass
137     addDeprecated.deprecated = True
138 
139     def addError(self, test, err):
140         """Called when a test raises an uncaught exception. DO NOT return a
141         value unless you want to stop other plugins from seeing that the
142         test has raised an error.
143         :param test: the test case
144         :type test: :class:`nose.case.Test`            
145         :param err: sys.exc_info() tuple
146         :type err: 3-tuple
147         """
148         pass
149     addError.changed = True
150 
151     def addFailure(self, test, err):
152         """Called when a test fails. DO NOT return a value unless you
153         want to stop other plugins from seeing that the test has failed.
154         :param test: the test case
155         :type test: :class:`nose.case.Test`
156         :param err: 3-tuple
157         :type err: sys.exc_info() tuple
158         """
159         pass
160     addFailure.changed = True
161 
162     def addSkip(self, test):
163         """Called when a test is skipped. DO NOT return a value unless
164         you want to stop other plugins from seeing the skipped test.
165         .. warning:: DEPRECATED -- check error class in addError instead
166         """
167         pass
168     addSkip.deprecated = True
169 
170     def addSuccess(self, test):
171         """Called when a test passes. DO NOT return a value unless you
172         want to stop other plugins from seeing the passing test.
173         :param test: the test case
174         :type test: :class:`nose.case.Test`
175         """
176         pass
177     addSuccess.changed = True
178 
179     def afterContext(self):
180         """Called after a context (generally a module) has been
181         lazy-loaded, imported, setup, had its tests loaded and
182         executed, and torn down.
183         """
184         pass
185     afterContext._new = True
186 
187     def afterDirectory(self, path):
188         """Called after all tests have been loaded from directory at path
189         and run.
190         :param path: the directory that has finished processing
191         :type path: string
192         """
193         pass
194     afterDirectory._new = True
195 
196     def afterImport(self, filename, module):
197         """Called after module is imported from filename. afterImport
198         is called even if the import failed.
199         :param filename: The file that was loaded
200         :type filename: string
201         :param module: The name of the module
202         :type module: string
203         """
204         pass
205     afterImport._new = True
206 
207     def afterTest(self, test):
208         """Called after the test has been run and the result recorded
209         (after stopTest).
210         :param test: the test case
211         :type test: :class:`nose.case.Test`
212         """
213         pass
214     afterTest._new = True
215 
216     def beforeContext(self):
217         """Called before a context (generally a module) is
218         examined. Because the context is not yet loaded, plugins don't
219         get to know what the context is; so any context operations
220         should use a stack that is pushed in `beforeContext` and popped
221         in `afterContext` to ensure they operate symmetrically.
222         `beforeContext` and `afterContext` are mainly useful for tracking
223         and restoring global state around possible changes from within a
224         context, whatever the context may be. If you need to operate on
225         contexts themselves, see `startContext` and `stopContext`, which
226         are passed the context in question, but are called after
227         it has been loaded (imported in the module case).
228         """
229         pass
230     beforeContext._new = True
231 
232     def beforeDirectory(self, path):
233         """Called before tests are loaded from directory at path.
234         :param path: the directory that is about to be processed
235         """
236         pass
237     beforeDirectory._new = True
238 
239     def beforeImport(self, filename, module):
240         """Called before module is imported from filename.
241         :param filename: The file that will be loaded
242         :param module: The name of the module found in file
243         :type module: string
244         """
245     beforeImport._new = True
246 
247     def beforeTest(self, test):
248         """Called before the test is run (before startTest).
249         :param test: the test case
250         :type test: :class:`nose.case.Test`
251         """
252         pass
253     beforeTest._new = True
254  
255     def begin(self):
256         """Called before any tests are collected or run. Use this to
257         perform any setup needed before testing begins.
258         """
259         pass
260 
261     def configure(self, options, conf):
262         """Called after the command line has been parsed, with the
263         parsed options and the config container. Here, implement any
264         config storage or changes to state or operation that are set
265         by command line options.
266         DO NOT return a value from this method unless you want to
267         stop all other plugins from being configured.
268         """
269         pass
270 
271     def finalize(self, result):
272         """Called after all report output, including output from all
273         plugins, has been sent to the stream. Use this to print final
274         test results or perform final cleanup. Return None to allow
275         other plugins to continue printing, or any other value to stop
276         them.
277         :param result: test result object
278         
279         .. Note:: When tests are run under a test runner other than
280            :class:`nose.core.TextTestRunner`, such as
281            via ``python setup.py test``, this method may be called
282            **before** the default report output is sent.
283         """
284         pass
285 
286     def describeTest(self, test):
287         """Return a test description.
288         Called by :meth:`nose.case.Test.shortDescription`.
289         :param test: the test case
290         :type test: :class:`nose.case.Test`
291         """
292         pass
293     describeTest._new = True
294 
295     def formatError(self, test, err):
296         """Called in result.addError, before plugin.addError. If you
297         want to replace or modify the error tuple, return a new error
298         tuple, otherwise return err, the original error tuple.
299         
300         :param test: the test case
301         :type test: :class:`nose.case.Test`
302         :param err: sys.exc_info() tuple
303         :type err: 3-tuple
304         """
305         pass
306     formatError._new = True
307     formatError.chainable = True
308     # test arg is not chainable
309     formatError.static_args = (True, False)
310 
311     def formatFailure(self, test, err):
312         """Called in result.addFailure, before plugin.addFailure. If you
313         want to replace or modify the error tuple, return a new error
314         tuple, otherwise return err, the original error tuple.
315         
316         :param test: the test case
317         :type test: :class:`nose.case.Test`
318         :param err: sys.exc_info() tuple
319         :type err: 3-tuple
320         """
321         pass
322     formatFailure._new = True
323     formatFailure.chainable = True
324     # test arg is not chainable
325     formatFailure.static_args = (True, False)
326 
327     def handleError(self, test, err):
328         """Called on addError. To handle the error yourself and prevent normal
329         error processing, return a true value.
330         :param test: the test case
331         :type test: :class:`nose.case.Test`
332         :param err: sys.exc_info() tuple
333         :type err: 3-tuple
334         """
335         pass
336     handleError._new = True
337 
338     def handleFailure(self, test, err):
339         """Called on addFailure. To handle the failure yourself and
340         prevent normal failure processing, return a true value.
341         :param test: the test case
342         :type test: :class:`nose.case.Test`
343         :param err: sys.exc_info() tuple
344         :type err: 3-tuple
345         """
346         pass
347     handleFailure._new = True
348 
349     def loadTestsFromDir(self, path):
350         """Return iterable of tests from a directory. May be a
351         generator.  Each item returned must be a runnable
352         unittest.TestCase (or subclass) instance or suite instance.
353         Return None if your plugin cannot collect any tests from
354         directory.
355         :param  path: The path to the directory.
356         """
357         pass
358     loadTestsFromDir.generative = True
359     loadTestsFromDir._new = True
360     
361     def loadTestsFromModule(self, module, path=None):
362         """Return iterable of tests in a module. May be a
363         generator. Each item returned must be a runnable
364         unittest.TestCase (or subclass) instance.
365         Return None if your plugin cannot
366         collect any tests from module.
367         :param module: The module object
368         :type module: python module
369         :param path: the path of the module to search, to distinguish from
370             namespace package modules
371             .. note::
372                NEW. The ``path`` parameter will only be passed by nose 0.11
373                or above.
374         """
375         pass
376     loadTestsFromModule.generative = True
377 
378     def loadTestsFromName(self, name, module=None, importPath=None):
379         """Return tests in this file or module. Return None if you are not able
380         to load any tests, or an iterable if you are. May be a
381         generator.
382         :param name: The test name. May be a file or module name plus a test
383             callable. Use split_test_name to split into parts. Or it might
384             be some crazy name of your own devising, in which case, do
385             whatever you want.
386         :param module: Module from which the name is to be loaded
387         :param importPath: Path from which file (must be a python module) was
388             found
389             .. warning:: DEPRECATED: this argument will NOT be passed.
390         """
391         pass
392     loadTestsFromName.generative = True
393 
394     def loadTestsFromNames(self, names, module=None):
395         """Return a tuple of (tests loaded, remaining names). Return
396         None if you are not able to load any tests. Multiple plugins
397         may implement loadTestsFromNames; the remaining name list from
398         each will be passed to the next as input.
399         :param names: List of test names.
400         :type names: iterable
401         :param module: Module from which the names are to be loaded
402         """
403         pass
404     loadTestsFromNames._new = True
405     loadTestsFromNames.chainable = True
406 
407     def loadTestsFromFile(self, filename):
408         """Return tests in this file. Return None if you are not
409         interested in loading any tests, or an iterable if you are and
410         can load some. May be a generator. *If you are interested in
411         loading tests from the file and encounter no errors, but find
412         no tests, yield False or return [False].*
413         .. Note:: This method replaces loadTestsFromPath from the 0.9
414                   API.
415         :param filename: The full path to the file or directory.
416         """
417         pass
418     loadTestsFromFile.generative = True
419     loadTestsFromFile._new = True
420 
421     def loadTestsFromPath(self, path):
422         """
423         .. warning:: DEPRECATED -- use loadTestsFromFile instead
424         """
425         pass
426     loadTestsFromPath.deprecated = True
427 
428     def loadTestsFromTestCase(self, cls):
429         """Return tests in this test case class. Return None if you are
430         not able to load any tests, or an iterable if you are. May be a
431         generator.
432         :param cls: The test case class. Must be subclass of
433            :class:`unittest.TestCase`.
434         """
435         pass
436     loadTestsFromTestCase.generative = True
437 
438     def loadTestsFromTestClass(self, cls):
439         """Return tests in this test class. Class will *not* be a
440         unittest.TestCase subclass. Return None if you are not able to
441         load any tests, an iterable if you are. May be a generator.
442         :param cls: The test case class. Must be **not** be subclass of
443            :class:`unittest.TestCase`.
444         """
445         pass
446     loadTestsFromTestClass._new = True
447     loadTestsFromTestClass.generative = True
448 
449     def makeTest(self, obj, parent):
450         """Given an object and its parent, return or yield one or more
451         test cases. Each test must be a unittest.TestCase (or subclass)
452         instance. This is called before default test loading to allow
453         plugins to load an alternate test case or cases for an
454         object. May be a generator.
455         :param obj: The object to be made into a test
456         :param parent: The parent of obj (eg, for a method, the class)
457         """
458         pass
459     makeTest._new = True
460     makeTest.generative = True
461 
462     def options(self, parser, env):
463         """Called to allow plugin to register command line
464         options with the parser.
465         DO NOT return a value from this method unless you want to stop
466         all other plugins from setting their options.
467         :param parser: options parser instance
468         :type parser: :class:`ConfigParser.ConfigParser`
469         :param env: environment, default is os.environ
470         """
471         pass
472     options._new = True
473 
474     def prepareTest(self, test):
475         """Called before the test is run by the test runner. Please
476         note the article *the* in the previous sentence: prepareTest
477         is called *only once*, and is passed the test case or test
478         suite that the test runner will execute. It is *not* called
479         for each individual test case. If you return a non-None value,
480         that return value will be run as the test. Use this hook to
481         wrap or decorate the test with another function. If you need
482         to modify or wrap individual test cases, use `prepareTestCase`
483         instead.
484         :param test: the test case
485         :type test: :class:`nose.case.Test`
486         """
487         pass
488 
489     def prepareTestCase(self, test):
490         """Prepare or wrap an individual test case. Called before
491         execution of the test. The test passed here is a
492         nose.case.Test instance; the case to be executed is in the
493         test attribute of the passed case. To modify the test to be
494         run, you should return a callable that takes one argument (the
495         test result object) -- it is recommended that you *do not*
496         side-effect the nose.case.Test instance you have been passed.
497         Keep in mind that when you replace the test callable you are
498         replacing the run() method of the test case -- including the
499         exception handling and result calls, etc.
500         :param test: the test case
501         :type test: :class:`nose.case.Test`
502         """
503         pass
504     prepareTestCase._new = True
505     
506     def prepareTestLoader(self, loader):
507         """Called before tests are loaded. To replace the test loader,
508         return a test loader. To allow other plugins to process the
509         test loader, return None. Only one plugin may replace the test
510         loader. Only valid when using nose.TestProgram.
511         :param loader: :class:`nose.loader.TestLoader` 
512              (or other loader) instance
513         """
514         pass
515     prepareTestLoader._new = True
516 
517     def prepareTestResult(self, result):
518         """Called before the first test is run. To use a different
519         test result handler for all tests than the given result,
520         return a test result handler. NOTE however that this handler
521         will only be seen by tests, that is, inside of the result
522         proxy system. The TestRunner and TestProgram -- whether nose's
523         or other -- will continue to see the original result
524         handler. For this reason, it is usually better to monkeypatch
525         the result (for instance, if you want to handle some
526         exceptions in a unique way). Only one plugin may replace the
527         result, but many may monkeypatch it. If you want to
528         monkeypatch and stop other plugins from doing so, monkeypatch
529         and return the patched result.
530         :param result: :class:`nose.result.TextTestResult` 
531              (or other result) instance
532         """
533         pass
534     prepareTestResult._new = True
535 
536     def prepareTestRunner(self, runner):
537         """Called before tests are run. To replace the test runner,
538         return a test runner. To allow other plugins to process the
539         test runner, return None. Only valid when using nose.TestProgram.
540         :param runner: :class:`nose.core.TextTestRunner` 
541              (or other runner) instance
542         """
543         pass
544     prepareTestRunner._new = True
545         
546     def report(self, stream):
547         """Called after all error output has been printed. Print your
548         plugin's report to the provided stream. Return None to allow
549         other plugins to print reports, any other value to stop them.
550         :param stream: stream object; send your output here
551         :type stream: file-like object
552         """
553         pass
554 
555     def setOutputStream(self, stream):
556         """Called before test output begins. To direct test output to a
557         new stream, return a stream object, which must implement a
558         `write(msg)` method. If you only want to note the stream, not
559         capture or redirect it, then return None.
560         :param stream: stream object; send your output here
561         :type stream: file-like object
562         """
563 
564     def startContext(self, context):
565         """Called before context setup and the running of tests in the
566         context. Note that tests have already been *loaded* from the
567         context before this call.
568         :param context: the context about to be setup. May be a module or
569              class, or any other object that contains tests.
570         """
571         pass
572     startContext._new = True
573     
574     def startTest(self, test):
575         """Called before each test is run. DO NOT return a value unless
576         you want to stop other plugins from seeing the test start.
577         :param test: the test case
578         :type test: :class:`nose.case.Test`
579         """
580         pass
581 
582     def stopContext(self, context):
583         """Called after the tests in a context have run and the
584         context has been torn down.
585         :param context: the context that has been torn down. May be a module or
586              class, or any other object that contains tests.
587         """
588         pass
589     stopContext._new = True
590     
591     def stopTest(self, test):
592         """Called after each test is run. DO NOT return a value unless
593         you want to stop other plugins from seeing that the test has stopped.
594         :param test: the test case
595         :type test: :class:`nose.case.Test`
596         """
597         pass
598 
599     def testName(self, test):
600         """Return a short test name. Called by `nose.case.Test.__str__`.
601         :param test: the test case
602         :type test: :class:`nose.case.Test`
603         """
604         pass
605     testName._new = True
606 
607     def wantClass(self, cls):
608         """Return true if you want the main test selector to collect
609         tests from this class, false if you don't, and None if you don't
610         care.
611         :param cls: The class being examined by the selector
612         """
613         pass
614     
615     def wantDirectory(self, dirname):
616         """Return true if you want test collection to descend into this
617         directory, false if you do not, and None if you don't care.
618         :param dirname: Full path to directory being examined by the selector
619         """
620         pass
621     
622     def wantFile(self, file):
623         """Return true if you want to collect tests from this file,
624         false if you do not and None if you don't care.
625         Change from 0.9: The optional package parameter is no longer passed.
626         :param file: Full path to file being examined by the selector
627         """
628         pass
629     
630     def wantFunction(self, function):
631         """Return true to collect this function as a test, false to
632         prevent it from being collected, and None if you don't care.
633         :param function: The function object being examined by the selector
634         """
635         pass
636     
637     def wantMethod(self, method):
638         """Return true to collect this method as a test, false to
639         prevent it from being collected, and None if you don't care.
640         
641         :param method: The method object being examined by the selector
642         :type method: unbound method
643         """    
644         pass
645     
646     def wantModule(self, module):
647         """Return true if you want to collection to descend into this
648         module, false to prevent the collector from descending into the
649         module, and None if you don't care.
650         :param module: The module object being examined by the selector
651         :type module: python module
652         """
653         pass
654     
655     def wantModuleTests(self, module):
656         """
657         .. warning:: DEPRECATED -- this method will not be called, it has
658                      been folded into wantModule.
659         """
660         pass
661     wantModuleTests.deprecated = True
nose.plugin.base.py

 

nose.plugin.manager.py模块源码

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

 

                                                                         图3-1-1 PluginManager类图

    manager的客户层是nose.core和nose.config模块,这两个模块通过plugin属性引用manger。通过plugin属性实现manager的控制,这两个模块的主要代码是命令

解析和环境变量驱动,代码杂乱不易于解析。在此, 抽象出plugin对象来走读manager代码。


#!/usr/bin/env python # -*- coding:utf-8 -*- from nose.plugin import manager def main(): plugin = manager.DefaultPluginManager() plugin.loadPlugins() plugin.addOptions() plugin.configure(options, config) plugin.begain() plugin.prepareTestLoader() if __name__ == "__main__": main()

 

 

3.1 PluginManager实例化与初始化

plugin = manager.DefaultPluginManager()

 

    * 客户端对Manager的实例化, 构造函数很简单初始化_plugin(存放插件的列表)和_proxies(存放代理元组的字典)属性. 类属性定义了代理类为PluginPoryx, 在这个地方引用了PluginProxy. 类声明的最后用property声明了plugin属性.

   所以实际上应该拥有3个属性(plugins, _plugin, proxyClass)

plugin.loadPlugins()

    * 客户端初始化确切的说是插件的初始化, 调用的是DefaultPluginManger的loadPlugins()方法, 这里有点意思:

    a.DefaultPluginManger多继承自EntryPointPluginManger和BuitinPluginManger, 这两个都是PluginManager的子类.如类图多继承自同源类. 这个有什么稀奇呢?

1 EntryPointPluginManager.loadPlugins()
2 BuitInPluginManager.loadPlugins()
3 PluginManager.loadPlugins()

       断点执行发现他的调用顺序很有意思: 首先调用EntryPoint的loadPlugins方法没什么问题, 按照多继承的MRO(Method Resolution Order)顺序, 广度优先执行第一个父类的方法. EntryPoint.loadPlugins内部调用super(EntryPoint, self).loadPlugins()居然

调用到了第二个父类的loadPlugins方法!!! 经过一番了解, 原来这里运用了多继承super的黑科技: 多继承多泰调用的时候, super会按照子类的MRO顺序传递. DefaultPluginManger的__mro__== [<EntryPoint>, <Buitin>, <PluginManager>]. 当调用super时会

根据当前的类, 调用__mro__列表中的下一个类的方法. 最后调用祖先类的方法.

       loadPlugins方法的最终效果是: 将内建的和符合nose.plugin组名的entry_points全部存放到DefaultPluginManger._plugins列表中.

plugin.addOptions()
plugin.configure(options, config)

      addOptions方法调用插件的addOptions(调用方式后述), 通过环境变量和sys.args参数给插件参数赋值, 其中enabled参数表征该插件是否开启. 可以通过三种方式: nose配置文件, 插件enabled默认值, 还有sys.args是否传入相关参数. configure方法调用插件

的configure方法, 然后过滤出enabled(开启)的插件, 通过插件的score属性排序存放到_plugins属性. 至此, 需要用到的插件全部找出来, 并且初始化, 按照优先级存放在列表中.

 3.2 插件的驱动调用

    以上步骤后, 插件已经存放在有序列表中了. 那么插件的方法是如何被调用的呢?  答案是出神入化的代理模式. 代理的入口是PluginManger的__getattr__函数. 客户曾调用PlugInManager的方法, 当调用到PluginInterface的方法时(PluginManager没有实现)则

会调用PluginManager的__getattr__方法. __getattr__返回PluginProxy(call, self._plugins), 实例化一个PluginProxy对象.

    PluginProxy的构造函数__init__(call, plugins)接收两个参数, 一个是plugin客户层调用的方法名(String类型) 和 一个存放在PluginManager的之前介绍的存放插件对象列表的PluginManager._plugins列表. PluginProxy依赖IPluginInterface类, 在构造函数中判断

该方法是否在IPluginInterface里面声明过, 如果不是直接抛异常结束. 然后是调用makeCall(call)方法, 这个方法其实就是个分支选择器, 根据call和self.meth的类型将调用指派给(chain, generate, _loadTestsFromNames和simple)具体的调用方式, 注意这里返回的

是函数引用(函数对象).  最后在self.plugins列表中存放二元组(插件对象plugin,  函数对象meth).

    客户层通过PluginManager.__getattr__返回一个PluginProxy对象, 之后再来调用就进入到PluginProxy的__call__方法. __call__方法调用之前的self.call(初始化时通过makeCall指派的函数). 一般调用到simple函数. simple函数遍历self.plugins中存放的二元组.

按照之前已经排好的顺序依次执行插件的某个方法(如PluginInterface.begain), 直到遇到第一个返回值. 至此, 实现插件方法的调用!!!

IV. 类比 

    回过头来, 类比我们的调色板模型. 

光格按照模板划分为16格供人填涂颜色, 就是PluginInterface模板罗列的扩展点供插件扩展.

透明片类比插件, 涂抹一个或者多个感兴趣的格子, 就是实现感兴趣的插件扩展点. 

透光合成器类比插件代理, 按照顺序依次穿过透明片, 经过同位置的格子, 最后合成这个位置上的颜色. 遇到黑色不再透光(第一个有返回值的函数).

栅格扫描器类比插件管理, 发现和收集插件, 按照分数排序, 过滤掉不开启的插件. 接收外界调用, 自己未实现的方法则代理给透光合成器.

V.总结

    插件的实现运用了AOP和代理模式, 很巧妙的把扩展点切片化, 把插件驱动运行层细化. 

    PluginInterface用到了协议模式, 只声明不允许实例化. 遵守该协议的类不允许继承或者引用该类, 只需要实现其中的方法即可.

 

posted @ 2021-03-02 13:45  O万山  阅读(168)  评论(0编辑  收藏  举报