1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

""" 

ErrorClass Plugins 

------------------ 

 

ErrorClass plugins provide an easy way to add support for custom 

handling of particular classes of exceptions. 

 

An ErrorClass plugin defines one or more ErrorClasses and how each is 

handled and reported on. Each error class is stored in a different 

attribute on the result, and reported separately. Each error class must 

indicate the exceptions that fall under that class, the label to use 

for reporting, and whether exceptions of the class should be 

considered as failures for the whole test run. 

 

ErrorClasses use a declarative syntax. Assign an ErrorClass to the 

attribute you wish to add to the result object, defining the 

exceptions, label and isfailure attributes. For example, to declare an 

ErrorClassPlugin that defines TodoErrors (and subclasses of TodoError) 

as an error class with the label 'TODO' that is considered a failure, 

do this: 

 

>>> class Todo(Exception): 

... pass 

>>> class TodoError(ErrorClassPlugin): 

... todo = ErrorClass(Todo, label='TODO', isfailure=True) 

 

The MetaErrorClass metaclass translates the ErrorClass declarations 

into the tuples used by the error handling and reporting functions in 

the result. This is an internal format and subject to change; you 

should always use the declarative syntax for attaching ErrorClasses to 

an ErrorClass plugin. 

 

>>> TodoError.errorClasses # doctest: +ELLIPSIS 

((<class ...Todo...>, ('todo', 'TODO', True)),) 

 

Let's see the plugin in action. First some boilerplate. 

 

>>> import sys 

>>> import unittest 

>>> try: 

... # 2.7+ 

... from unittest.runner import _WritelnDecorator 

... except ImportError: 

... from unittest import _WritelnDecorator 

...  

>>> buf = _WritelnDecorator(sys.stdout) 

 

Now define a test case that raises a Todo. 

 

>>> class TestTodo(unittest.TestCase): 

... def runTest(self): 

... raise Todo("I need to test something") 

>>> case = TestTodo() 

 

Prepare the result using our plugin. Normally this happens during the 

course of test execution within nose -- you won't be doing this 

yourself. For the purposes of this testing document, I'm stepping 

through the internal process of nose so you can see what happens at 

each step. 

 

>>> plugin = TodoError() 

>>> from nose.result import _TextTestResult 

>>> result = _TextTestResult(stream=buf, descriptions=0, verbosity=2) 

>>> plugin.prepareTestResult(result) 

 

Now run the test. TODO is printed. 

 

>>> _ = case(result) # doctest: +ELLIPSIS 

runTest (....TestTodo) ... TODO: I need to test something 

 

Errors and failures are empty, but todo has our test: 

 

>>> result.errors 

[] 

>>> result.failures 

[] 

>>> result.todo # doctest: +ELLIPSIS 

[(<....TestTodo testMethod=runTest>, '...Todo: I need to test something\\n')] 

>>> result.printErrors() # doctest: +ELLIPSIS 

<BLANKLINE> 

====================================================================== 

TODO: runTest (....TestTodo) 

---------------------------------------------------------------------- 

Traceback (most recent call last): 

... 

...Todo: I need to test something 

<BLANKLINE> 

 

Since we defined a Todo as a failure, the run was not successful. 

 

>>> result.wasSuccessful() 

False 

""" 

 

from nose.pyversion import make_instancemethod 

from nose.plugins.base import Plugin 

from nose.result import TextTestResult 

from nose.util import isclass 

 

class MetaErrorClass(type): 

"""Metaclass for ErrorClassPlugins that allows error classes to be 

set up in a declarative manner. 

""" 

def __init__(self, name, bases, attr): 

errorClasses = [] 

for name, detail in list(attr.items()): 

if isinstance(detail, ErrorClass): 

attr.pop(name) 

for cls in detail: 

errorClasses.append( 

(cls, (name, detail.label, detail.isfailure))) 

super(MetaErrorClass, self).__init__(name, bases, attr) 

self.errorClasses = tuple(errorClasses) 

 

 

class ErrorClass(object): 

def __init__(self, *errorClasses, **kw): 

self.errorClasses = errorClasses 

try: 

for key in ('label', 'isfailure'): 

setattr(self, key, kw.pop(key)) 

except KeyError: 

raise TypeError("%r is a required named argument for ErrorClass" 

% key) 

 

def __iter__(self): 

return iter(self.errorClasses) 

 

 

class ErrorClassPlugin(Plugin, metaclass=MetaErrorClass): 

""" 

Base class for ErrorClass plugins. Subclass this class and declare the 

exceptions that you wish to handle as attributes of the subclass. 

""" 

score = 1000 

errorClasses = () 

 

def addError(self, test, err): 

err_cls, a, b = err 

if not isclass(err_cls): 

return 

classes = [e[0] for e in self.errorClasses] 

if [c for c in classes if issubclass(err_cls, c)]: 

return True 

 

def prepareTestResult(self, result): 

if not hasattr(result, 'errorClasses'): 

self.patchResult(result) 

for cls, (storage_attr, label, isfail) in self.errorClasses: 

if cls not in result.errorClasses: 

storage = getattr(result, storage_attr, []) 

setattr(result, storage_attr, storage) 

result.errorClasses[cls] = (storage, label, isfail) 

 

def patchResult(self, result): 

result.printLabel = print_label_patch(result) 

result._orig_addError, result.addError = \ 

result.addError, add_error_patch(result) 

result._orig_wasSuccessful, result.wasSuccessful = \ 

result.wasSuccessful, wassuccessful_patch(result) 

if hasattr(result, 'printErrors'): 

result._orig_printErrors, result.printErrors = \ 

result.printErrors, print_errors_patch(result) 

if hasattr(result, 'addSkip'): 

result._orig_addSkip, result.addSkip = \ 

result.addSkip, add_skip_patch(result) 

result.errorClasses = {} 

 

 

def add_error_patch(result): 

"""Create a new addError method to patch into a result instance 

that recognizes the errorClasses attribute and deals with 

errorclasses correctly. 

""" 

return make_instancemethod(TextTestResult.addError, result) 

 

 

def print_errors_patch(result): 

"""Create a new printErrors method that prints errorClasses items 

as well. 

""" 

return make_instancemethod(TextTestResult.printErrors, result) 

 

 

def print_label_patch(result): 

"""Create a new printLabel method that prints errorClasses items 

as well. 

""" 

return make_instancemethod(TextTestResult.printLabel, result) 

 

 

def wassuccessful_patch(result): 

"""Create a new wasSuccessful method that checks errorClasses for 

exceptions that were put into other slots than error or failure 

but that still count as not success. 

""" 

return make_instancemethod(TextTestResult.wasSuccessful, result) 

 

 

def add_skip_patch(result): 

"""Create a new addSkip method to patch into a result instance 

that delegates to addError. 

""" 

return make_instancemethod(TextTestResult.addSkip, result) 

 

 

if __name__ == '__main__': 

import doctest 

doctest.testmod()