Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/assertion/__init__.py: 71%

86 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-05-04 13:14 +0700

1"""Support for presenting detailed information in failing assertions.""" 

2import sys 

3from typing import Any 

4from typing import Generator 

5from typing import List 

6from typing import Optional 

7from typing import TYPE_CHECKING 

8 

9from _pytest.assertion import rewrite 

10from _pytest.assertion import truncate 

11from _pytest.assertion import util 

12from _pytest.assertion.rewrite import assertstate_key 

13from _pytest.config import Config 

14from _pytest.config import hookimpl 

15from _pytest.config.argparsing import Parser 

16from _pytest.nodes import Item 

17 

18if TYPE_CHECKING: 

19 from _pytest.main import Session 

20 

21 

22def pytest_addoption(parser: Parser) -> None: 

23 group = parser.getgroup("debugconfig") 

24 group.addoption( 

25 "--assert", 

26 action="store", 

27 dest="assertmode", 

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

29 default="rewrite", 

30 metavar="MODE", 

31 help=( 

32 "Control assertion debugging tools.\n" 

33 "'plain' performs no assertion debugging.\n" 

34 "'rewrite' (the default) rewrites assert statements in test modules" 

35 " on import to provide assert expression information." 

36 ), 

37 ) 

38 parser.addini( 

39 "enable_assertion_pass_hook", 

40 type="bool", 

41 default=False, 

42 help="Enables the pytest_assertion_pass hook. " 

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

44 ) 

45 

46 

47def register_assert_rewrite(*names: str) -> None: 

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

49 

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

51 the package will get their assert statements rewritten. 

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

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

54 using a package. 

55 

56 :param names: The module names to register. 

57 """ 

58 for name in names: 

59 if not isinstance(name, str): 

60 msg = "expected module names as *args, got {0} instead" # type: ignore[unreachable] 

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

62 for hook in sys.meta_path: 

63 if isinstance(hook, rewrite.AssertionRewritingHook): 

64 importhook = hook 

65 break 

66 else: 

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

68 # for importhook and for PytestPluginManager.rewrite_hook. 

69 importhook = DummyRewriteHook() # type: ignore 

70 importhook.mark_rewrite(*names) 

71 

72 

73class DummyRewriteHook: 

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

75 

76 def mark_rewrite(self, *names: str) -> None: 

77 pass 

78 

79 

80class AssertionState: 

81 """State for the assertion plugin.""" 

82 

83 def __init__(self, config: Config, mode) -> None: 

84 self.mode = mode 

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

86 self.hook: Optional[rewrite.AssertionRewritingHook] = None 

87 

88 

89def install_importhook(config: Config) -> rewrite.AssertionRewritingHook: 

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

91 config.stash[assertstate_key] = AssertionState(config, "rewrite") 

92 config.stash[assertstate_key].hook = hook = rewrite.AssertionRewritingHook(config) 

93 sys.meta_path.insert(0, hook) 

94 config.stash[assertstate_key].trace("installed rewrite import hook") 

95 

96 def undo() -> None: 

97 hook = config.stash[assertstate_key].hook 

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

99 sys.meta_path.remove(hook) 

100 

101 config.add_cleanup(undo) 

102 return hook 

103 

104 

105def pytest_collection(session: "Session") -> None: 

106 # This hook is only called when test modules are collected 

107 # so for example not in the managing process of pytest-xdist 

108 # (which does not collect test modules). 

109 assertstate = session.config.stash.get(assertstate_key, None) 

110 if assertstate: 

111 if assertstate.hook is not None: 

112 assertstate.hook.set_session(session) 

113 

114 

115@hookimpl(tryfirst=True, hookwrapper=True) 

116def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: 

117 """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks. 

118 

119 The rewrite module will use util._reprcompare if it exists to use custom 

120 reporting via the pytest_assertrepr_compare hook. This sets up this custom 

121 comparison for the test. 

122 """ 

123 

124 ihook = item.ihook 

125 

126 def callbinrepr(op, left: object, right: object) -> Optional[str]: 

127 """Call the pytest_assertrepr_compare hook and prepare the result. 

128 

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

130 following: 

131 * Overly verbose explanations are truncated unless configured otherwise 

132 (eg. if running in verbose mode). 

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

134 later. 

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

136 to protect later % formatting. 

137 

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

139 pretty printing. 

140 """ 

141 hook_result = ihook.pytest_assertrepr_compare( 

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

143 ) 

144 for new_expl in hook_result: 

145 if new_expl: 

146 new_expl = truncate.truncate_if_required(new_expl, item) 

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

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

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

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

151 return res 

152 return None 

153 

154 saved_assert_hooks = util._reprcompare, util._assertion_pass 

155 util._reprcompare = callbinrepr 

156 util._config = item.config 

157 

158 if ihook.pytest_assertion_pass.get_hookimpls(): 

159 

160 def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None: 

161 ihook.pytest_assertion_pass(item=item, lineno=lineno, orig=orig, expl=expl) 

162 

163 util._assertion_pass = call_assertion_pass_hook 

164 

165 yield 

166 

167 util._reprcompare, util._assertion_pass = saved_assert_hooks 

168 util._config = None 

169 

170 

171def pytest_sessionfinish(session: "Session") -> None: 

172 assertstate = session.config.stash.get(assertstate_key, None) 

173 if assertstate: 

174 if assertstate.hook is not None: 

175 assertstate.hook.set_session(None) 

176 

177 

178def pytest_assertrepr_compare( 

179 config: Config, op: str, left: Any, right: Any 

180) -> Optional[List[str]]: 

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