0
# Plugin Development
1
2
Framework for creating custom plugins that extend nose2's functionality through a comprehensive hook system and event-driven architecture. Plugins can modify behavior at every stage of test execution.
3
4
## Capabilities
5
6
### Plugin Base Class
7
8
All nose2 plugins must inherit from the Plugin base class and implement hook methods to extend functionality.
9
10
```python { .api }
11
class Plugin:
12
"""
13
Base class for nose2 plugins.
14
15
All nose2 plugins must subclass this class and implement hook methods
16
to extend test execution functionality.
17
"""
18
19
# Class attributes for plugin configuration
20
commandLineSwitch: tuple # (short_opt, long_opt, help_text)
21
configSection: str # Config file section name
22
alwaysOn: bool # Auto-register plugin flag
23
24
# Instance attributes set during initialization
25
session: Session # Test run session
26
config: Config # Plugin configuration section
27
28
def __init__(self, **kwargs):
29
"""
30
Initialize plugin.
31
32
Config values should be extracted from self.config in __init__
33
for sphinx documentation generation to work properly.
34
"""
35
36
def register(self):
37
"""
38
Register plugin with session hooks.
39
40
Called automatically if alwaysOn=True or command line switch is used.
41
"""
42
43
def addOption(self, callback, short_opt, long_opt, help_text=None, nargs=0):
44
"""
45
Add command line option.
46
47
Parameters:
48
- callback: Function to call when option is used, or list to append values to
49
- short_opt: Single character short option (must be uppercase, no dashes)
50
- long_opt: Long option name (without dashes)
51
- help_text: Help text for option
52
- nargs: Number of arguments (default: 0)
53
"""
54
55
def addArgument(self, callback, short_opt, long_opt, help_text=None):
56
"""
57
Add command line option that takes one argument.
58
59
Parameters:
60
- callback: Function to call when option is used (receives one argument)
61
- short_opt: Single character short option (must be uppercase, no dashes)
62
- long_opt: Long option name (without dashes)
63
- help_text: Help text for option
64
"""
65
```
66
67
### Session Management
68
69
The Session class coordinates plugin loading, configuration, and execution.
70
71
```python { .api }
72
class Session:
73
"""
74
Configuration session that encapsulates all configuration for a test run.
75
"""
76
77
# Core attributes
78
argparse: argparse.ArgumentParser # Command line parser
79
pluginargs: argparse.ArgumentGroup # Plugin argument group
80
hooks: PluginInterface # Plugin hook interface
81
plugins: list # List of loaded plugins
82
config: ConfigParser # Configuration parser
83
84
# Test run configuration
85
verbosity: int # Verbosity level
86
startDir: str # Test discovery start directory
87
topLevelDir: str # Top-level project directory
88
testResult: PluggableTestResult # Test result instance
89
testLoader: PluggableTestLoader # Test loader instance
90
logLevel: int # Logging level
91
92
def __init__(self):
93
"""Initialize session with default configuration."""
94
95
def get(self, section):
96
"""
97
Get a config section.
98
99
Parameters:
100
- section: The section name to retrieve
101
102
Returns:
103
Config instance for the section
104
"""
105
106
def loadConfigFiles(self, *filenames):
107
"""
108
Load configuration from files.
109
110
Parameters:
111
- filenames: Configuration file paths to load
112
"""
113
114
def loadPlugins(self, plugins, exclude):
115
"""
116
Load plugins into the session.
117
118
Parameters:
119
- plugins: List of plugin module names to load
120
- exclude: List of plugin module names to exclude
121
"""
122
123
def setVerbosity(self, verbosity, verbose, quiet):
124
"""
125
Set verbosity level from configuration and command line.
126
127
Parameters:
128
- verbosity: Base verbosity level
129
- verbose: Number of -v flags
130
- quiet: Number of -q flags
131
"""
132
133
def isPluginLoaded(self, plugin_name):
134
"""
135
Check if a plugin is loaded.
136
137
Parameters:
138
- plugin_name: Full plugin module name
139
140
Returns:
141
True if plugin is loaded, False otherwise
142
"""
143
```
144
145
### Plugin Interface and Hooks
146
147
The PluginInterface provides the hook system for plugin method calls.
148
149
```python { .api }
150
class PluginInterface:
151
"""Interface for plugin method hooks."""
152
153
def register(self, method_name, plugin):
154
"""
155
Register a plugin method for a hook.
156
157
Parameters:
158
- method_name: Name of hook method
159
- plugin: Plugin instance to register
160
"""
161
162
# Hook methods (examples - many more available)
163
def loadTestsFromModule(self, event):
164
"""Called when loading tests from a module."""
165
166
def loadTestsFromName(self, event):
167
"""Called when loading tests from a name."""
168
169
def startTest(self, event):
170
"""Called when a test starts."""
171
172
def stopTest(self, event):
173
"""Called when a test stops."""
174
175
def testOutcome(self, event):
176
"""Called when a test completes with an outcome."""
177
178
def createTests(self, event):
179
"""Called to create the top-level test suite."""
180
181
def runnerCreated(self, event):
182
"""Called when test runner is created."""
183
```
184
185
### Event Classes
186
187
Events carry information between the test framework and plugins.
188
189
```python { .api }
190
class Event:
191
"""Base class for all plugin events."""
192
193
handled: bool # Set to True if plugin handles the event
194
195
class LoadFromModuleEvent(Event):
196
"""Event fired when loading tests from a module."""
197
198
def __init__(self, loader, module):
199
self.loader = loader
200
self.module = module
201
self.extraTests = []
202
203
class StartTestEvent(Event):
204
"""Event fired when a test starts."""
205
206
def __init__(self, test, result, startTime):
207
self.test = test
208
self.result = result
209
self.startTime = startTime
210
211
class TestOutcomeEvent(Event):
212
"""Event fired when a test completes."""
213
214
def __init__(self, test, result, outcome, err=None, reason=None, expected=None):
215
self.test = test
216
self.result = result
217
self.outcome = outcome # 'error', 'failed', 'skipped', 'passed', 'subtest'
218
self.err = err
219
self.reason = reason
220
self.expected = expected
221
```
222
223
## Usage Examples
224
225
### Simple Plugin
226
227
```python
228
from nose2.events import Plugin
229
230
class TimingPlugin(Plugin):
231
"""Plugin that times test execution."""
232
233
configSection = 'timing'
234
commandLineSwitch = ('T', 'timing', 'Time test execution')
235
236
def __init__(self):
237
# Extract config values in __init__
238
self.enabled = self.config.as_bool('enabled', default=True)
239
self.threshold = self.config.as_float('threshold', default=1.0)
240
self.times = {}
241
242
def startTest(self, event):
243
"""Record test start time."""
244
import time
245
self.times[event.test] = time.time()
246
247
def stopTest(self, event):
248
"""Calculate and report test time."""
249
import time
250
if event.test in self.times:
251
elapsed = time.time() - self.times[event.test]
252
if elapsed > self.threshold:
253
print(f"SLOW: {event.test} took {elapsed:.2f}s")
254
del self.times[event.test]
255
```
256
257
### Configuration-Based Plugin
258
259
```python
260
from nose2.events import Plugin
261
262
class DatabasePlugin(Plugin):
263
"""Plugin for database test setup."""
264
265
configSection = 'database'
266
alwaysOn = True # Always load this plugin
267
268
def __init__(self):
269
# Read configuration
270
self.db_url = self.config.as_str('url', default='sqlite:///:memory:')
271
self.reset_db = self.config.as_bool('reset', default=True)
272
self.fixtures = self.config.as_list('fixtures', default=[])
273
274
# Add command line options
275
self.addArgument('--db-url', help='Database URL')
276
self.addArgument('--no-db-reset', action='store_true',
277
help='Skip database reset')
278
279
def handleArgs(self, event):
280
"""Handle command line arguments."""
281
args = event.args
282
if hasattr(args, 'db_url') and args.db_url:
283
self.db_url = args.db_url
284
if hasattr(args, 'no_db_reset') and args.no_db_reset:
285
self.reset_db = False
286
287
def createTests(self, event):
288
"""Set up database before creating tests."""
289
if self.reset_db:
290
self.setup_database()
291
292
def setup_database(self):
293
"""Initialize database with fixtures."""
294
# Database setup code
295
pass
296
```
297
298
### Test Loader Plugin
299
300
```python
301
from nose2.events import Plugin
302
303
class CustomLoaderPlugin(Plugin):
304
"""Custom test loader plugin."""
305
306
configSection = 'custom-loader'
307
commandLineSwitch = ('C', 'custom-loader', 'Use custom test loader')
308
309
def __init__(self):
310
self.pattern = self.config.as_str('pattern', default='spec_*.py')
311
self.base_class = self.config.as_str('base_class', default='Specification')
312
313
def loadTestsFromModule(self, event):
314
"""Load tests using custom pattern."""
315
module = event.module
316
tests = []
317
318
# Custom loading logic
319
for name in dir(module):
320
obj = getattr(module, name)
321
if (isinstance(obj, type) and
322
issubclass(obj, unittest.TestCase) and
323
obj.__name__.startswith(self.base_class)):
324
325
suite = self.session.testLoader.loadTestsFromTestCase(obj)
326
tests.append(suite)
327
328
if tests:
329
event.extraTests.extend(tests)
330
```
331
332
### Result Plugin
333
334
```python
335
from nose2.events import Plugin
336
337
class CustomResultPlugin(Plugin):
338
"""Custom test result reporter."""
339
340
configSection = 'custom-result'
341
342
def __init__(self):
343
self.output_file = self.config.as_str('output', default='results.txt')
344
self.include_passed = self.config.as_bool('include_passed', default=False)
345
self.results = []
346
347
def testOutcome(self, event):
348
"""Record test outcomes."""
349
test_info = {
350
'test': str(event.test),
351
'outcome': event.outcome,
352
'error': str(event.err) if event.err else None,
353
'reason': event.reason
354
}
355
356
if event.outcome != 'passed' or self.include_passed:
357
self.results.append(test_info)
358
359
def afterTestRun(self, event):
360
"""Write results to file after test run."""
361
with open(self.output_file, 'w') as f:
362
for result in self.results:
363
f.write(f"{result['test']}: {result['outcome']}\n")
364
if result['error']:
365
f.write(f" Error: {result['error']}\n")
366
if result['reason']:
367
f.write(f" Reason: {result['reason']}\n")
368
```
369
370
### Plugin Registration
371
372
```python
373
# In your plugin module (e.g., my_plugins.py)
374
from nose2.events import Plugin
375
376
class MyPlugin(Plugin):
377
configSection = 'my-plugin'
378
379
def __init__(self):
380
pass
381
382
def startTest(self, event):
383
print(f"Starting test: {event.test}")
384
385
# To use the plugin:
386
# 1. Via command line: nose2 --plugin my_plugins.MyPlugin
387
# 2. Via config file:
388
# [unittest]
389
# plugins = my_plugins.MyPlugin
390
# 3. Via programmatic loading:
391
# from nose2.main import PluggableTestProgram
392
# PluggableTestProgram(plugins=['my_plugins.MyPlugin'])
393
```
394
395
### Plugin Configuration
396
397
```ini
398
# unittest.cfg or nose2.cfg
399
[unittest]
400
plugins = my_plugins.TimingPlugin
401
my_plugins.DatabasePlugin
402
403
[timing]
404
enabled = true
405
threshold = 0.5
406
407
[database]
408
url = postgresql://localhost/test_db
409
reset = true
410
fixtures = users.json
411
products.json
412
413
[my-plugin]
414
some_option = value
415
flag = true
416
```
417
418
### Advanced Plugin Patterns
419
420
```python
421
from nose2.events import Plugin
422
423
class LayeredPlugin(Plugin):
424
"""Plugin that works with test layers."""
425
426
def startTestRun(self, event):
427
"""Called at the start of the test run."""
428
self.setup_resources()
429
430
def stopTestRun(self, event):
431
"""Called at the end of the test run."""
432
self.cleanup_resources()
433
434
def startTestClass(self, event):
435
"""Called when starting tests in a test class."""
436
if hasattr(event.testClass, 'layer'):
437
self.setup_layer(event.testClass.layer)
438
439
def stopTestClass(self, event):
440
"""Called when finishing tests in a test class."""
441
if hasattr(event.testClass, 'layer'):
442
self.teardown_layer(event.testClass.layer)
443
444
def setup_resources(self):
445
"""Set up resources for entire test run."""
446
pass
447
448
def cleanup_resources(self):
449
"""Clean up resources after test run."""
450
pass
451
452
def setup_layer(self, layer):
453
"""Set up resources for a test layer."""
454
pass
455
456
def teardown_layer(self, layer):
457
"""Tear down resources for a test layer."""
458
pass
459
```