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.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
图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用到了协议模式, 只声明不允许实例化. 遵守该协议的类不允许继承或者引用该类, 只需要实现其中的方法即可.