Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2support for presenting detailed information in failing assertions. 

3""" 

4import sys 

5from typing import Optional 

6 

7from _pytest.assertion import rewrite 

8from _pytest.assertion import truncate 

9from _pytest.assertion import util 

10 

11 

12def pytest_addoption(parser): 

13 group = parser.getgroup("debugconfig") 

14 group.addoption( 

15 "--assert", 

16 action="store", 

17 dest="assertmode", 

18 choices=("rewrite", "plain"), 

19 default="rewrite", 

20 metavar="MODE", 

21 help="""Control assertion debugging tools. 'plain' 

22 performs no assertion debugging. 'rewrite' 

23 (the default) rewrites assert statements in 

24 test modules on import to provide assert 

25 expression information.""", 

26 ) 

27 parser.addini( 

28 "enable_assertion_pass_hook", 

29 type="bool", 

30 default=False, 

31 help="Enables the pytest_assertion_pass hook." 

32 "Make sure to delete any previously generated pyc cache files.", 

33 ) 

34 

35 

36def register_assert_rewrite(*names) -> None: 

37 """Register one or more module names to be rewritten on import. 

38 

39 This function will make sure that this module or all modules inside 

40 the package will get their assert statements rewritten. 

41 Thus you should make sure to call this before the module is 

42 actually imported, usually in your __init__.py if you are a plugin 

43 using a package. 

44 

45 :raise TypeError: if the given module names are not strings. 

46 """ 

47 for name in names: 

48 if not isinstance(name, str): 

49 msg = "expected module names as *args, got {0} instead" 

50 raise TypeError(msg.format(repr(names))) 

51 for hook in sys.meta_path: 

52 if isinstance(hook, rewrite.AssertionRewritingHook): 

53 importhook = hook 

54 break 

55 else: 

56 # TODO(typing): Add a protocol for mark_rewrite() and use it 

57 # for importhook and for PytestPluginManager.rewrite_hook. 

58 importhook = DummyRewriteHook() # type: ignore 

59 importhook.mark_rewrite(*names) 

60 

61 

62class DummyRewriteHook: 

63 """A no-op import hook for when rewriting is disabled.""" 

64 

65 def mark_rewrite(self, *names): 

66 pass 

67 

68 

69class AssertionState: 

70 """State for the assertion plugin.""" 

71 

72 def __init__(self, config, mode): 

73 self.mode = mode 

74 self.trace = config.trace.root.get("assertion") 

75 self.hook = None # type: Optional[rewrite.AssertionRewritingHook] 

76 

77 

78def install_importhook(config): 

79 """Try to install the rewrite hook, raise SystemError if it fails.""" 

80 config._assertstate = AssertionState(config, "rewrite") 

81 config._assertstate.hook = hook = rewrite.AssertionRewritingHook(config) 

82 sys.meta_path.insert(0, hook) 

83 config._assertstate.trace("installed rewrite import hook") 

84 

85 def undo(): 

86 hook = config._assertstate.hook 

87 if hook is not None and hook in sys.meta_path: 

88 sys.meta_path.remove(hook) 

89 

90 config.add_cleanup(undo) 

91 return hook 

92 

93 

94def pytest_collection(session): 

95 # this hook is only called when test modules are collected 

96 # so for example not in the master process of pytest-xdist 

97 # (which does not collect test modules) 

98 assertstate = getattr(session.config, "_assertstate", None) 

99 if assertstate: 

100 if assertstate.hook is not None: 

101 assertstate.hook.set_session(session) 

102 

103 

104def pytest_runtest_setup(item): 

105 """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks 

106 

107 The newinterpret and rewrite modules will use util._reprcompare if 

108 it exists to use custom reporting via the 

109 pytest_assertrepr_compare hook. This sets up this custom 

110 comparison for the test. 

111 """ 

112 

113 def callbinrepr(op, left, right): 

114 # type: (str, object, object) -> Optional[str] 

115 """Call the pytest_assertrepr_compare hook and prepare the result 

116 

117 This uses the first result from the hook and then ensures the 

118 following: 

119 * Overly verbose explanations are truncated unless configured otherwise 

120 (eg. if running in verbose mode). 

121 * Embedded newlines are escaped to help util.format_explanation() 

122 later. 

123 * If the rewrite mode is used embedded %-characters are replaced 

124 to protect later % formatting. 

125 

126 The result can be formatted by util.format_explanation() for 

127 pretty printing. 

128 """ 

129 hook_result = item.ihook.pytest_assertrepr_compare( 

130 config=item.config, op=op, left=left, right=right 

131 ) 

132 for new_expl in hook_result: 

133 if new_expl: 

134 new_expl = truncate.truncate_if_required(new_expl, item) 

135 new_expl = [line.replace("\n", "\\n") for line in new_expl] 

136 res = "\n~".join(new_expl) 

137 if item.config.getvalue("assertmode") == "rewrite": 

138 res = res.replace("%", "%%") 

139 return res 

140 return None 

141 

142 util._reprcompare = callbinrepr 

143 

144 if item.ihook.pytest_assertion_pass.get_hookimpls(): 

145 

146 def call_assertion_pass_hook(lineno, orig, expl): 

147 item.ihook.pytest_assertion_pass( 

148 item=item, lineno=lineno, orig=orig, expl=expl 

149 ) 

150 

151 util._assertion_pass = call_assertion_pass_hook 

152 

153 

154def pytest_runtest_teardown(item): 

155 util._reprcompare = None 

156 util._assertion_pass = None 

157 

158 

159def pytest_sessionfinish(session): 

160 assertstate = getattr(session.config, "_assertstate", None) 

161 if assertstate: 

162 if assertstate.hook is not None: 

163 assertstate.hook.set_session(None) 

164 

165 

166def pytest_assertrepr_compare(config, op, left, right): 

167 return util.assertrepr_compare(config=config, op=op, left=left, right=right)