Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pyparsing/testing.py: 16%

122 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-08 06:51 +0000

1# testing.py 

2 

3from contextlib import contextmanager 

4import typing 

5 

6from .core import ( 

7 ParserElement, 

8 ParseException, 

9 Keyword, 

10 __diag__, 

11 __compat__, 

12) 

13 

14 

15class pyparsing_test: 

16 """ 

17 namespace class for classes useful in writing unit tests 

18 """ 

19 

20 class reset_pyparsing_context: 

21 """ 

22 Context manager to be used when writing unit tests that modify pyparsing config values: 

23 - packrat parsing 

24 - bounded recursion parsing 

25 - default whitespace characters. 

26 - default keyword characters 

27 - literal string auto-conversion class 

28 - __diag__ settings 

29 

30 Example:: 

31 

32 with reset_pyparsing_context(): 

33 # test that literals used to construct a grammar are automatically suppressed 

34 ParserElement.inlineLiteralsUsing(Suppress) 

35 

36 term = Word(alphas) | Word(nums) 

37 group = Group('(' + term[...] + ')') 

38 

39 # assert that the '()' characters are not included in the parsed tokens 

40 self.assertParseAndCheckList(group, "(abc 123 def)", ['abc', '123', 'def']) 

41 

42 # after exiting context manager, literals are converted to Literal expressions again 

43 """ 

44 

45 def __init__(self): 

46 self._save_context = {} 

47 

48 def save(self): 

49 self._save_context["default_whitespace"] = ParserElement.DEFAULT_WHITE_CHARS 

50 self._save_context["default_keyword_chars"] = Keyword.DEFAULT_KEYWORD_CHARS 

51 

52 self._save_context[ 

53 "literal_string_class" 

54 ] = ParserElement._literalStringClass 

55 

56 self._save_context["verbose_stacktrace"] = ParserElement.verbose_stacktrace 

57 

58 self._save_context["packrat_enabled"] = ParserElement._packratEnabled 

59 if ParserElement._packratEnabled: 

60 self._save_context[ 

61 "packrat_cache_size" 

62 ] = ParserElement.packrat_cache.size 

63 else: 

64 self._save_context["packrat_cache_size"] = None 

65 self._save_context["packrat_parse"] = ParserElement._parse 

66 self._save_context[ 

67 "recursion_enabled" 

68 ] = ParserElement._left_recursion_enabled 

69 

70 self._save_context["__diag__"] = { 

71 name: getattr(__diag__, name) for name in __diag__._all_names 

72 } 

73 

74 self._save_context["__compat__"] = { 

75 "collect_all_And_tokens": __compat__.collect_all_And_tokens 

76 } 

77 

78 return self 

79 

80 def restore(self): 

81 # reset pyparsing global state 

82 if ( 

83 ParserElement.DEFAULT_WHITE_CHARS 

84 != self._save_context["default_whitespace"] 

85 ): 

86 ParserElement.set_default_whitespace_chars( 

87 self._save_context["default_whitespace"] 

88 ) 

89 

90 ParserElement.verbose_stacktrace = self._save_context["verbose_stacktrace"] 

91 

92 Keyword.DEFAULT_KEYWORD_CHARS = self._save_context["default_keyword_chars"] 

93 ParserElement.inlineLiteralsUsing( 

94 self._save_context["literal_string_class"] 

95 ) 

96 

97 for name, value in self._save_context["__diag__"].items(): 

98 (__diag__.enable if value else __diag__.disable)(name) 

99 

100 ParserElement._packratEnabled = False 

101 if self._save_context["packrat_enabled"]: 

102 ParserElement.enable_packrat(self._save_context["packrat_cache_size"]) 

103 else: 

104 ParserElement._parse = self._save_context["packrat_parse"] 

105 ParserElement._left_recursion_enabled = self._save_context[ 

106 "recursion_enabled" 

107 ] 

108 

109 __compat__.collect_all_And_tokens = self._save_context["__compat__"] 

110 

111 return self 

112 

113 def copy(self): 

114 ret = type(self)() 

115 ret._save_context.update(self._save_context) 

116 return ret 

117 

118 def __enter__(self): 

119 return self.save() 

120 

121 def __exit__(self, *args): 

122 self.restore() 

123 

124 class TestParseResultsAsserts: 

125 """ 

126 A mixin class to add parse results assertion methods to normal unittest.TestCase classes. 

127 """ 

128 

129 def assertParseResultsEquals( 

130 self, result, expected_list=None, expected_dict=None, msg=None 

131 ): 

132 """ 

133 Unit test assertion to compare a :class:`ParseResults` object with an optional ``expected_list``, 

134 and compare any defined results names with an optional ``expected_dict``. 

135 """ 

136 if expected_list is not None: 

137 self.assertEqual(expected_list, result.as_list(), msg=msg) 

138 if expected_dict is not None: 

139 self.assertEqual(expected_dict, result.as_dict(), msg=msg) 

140 

141 def assertParseAndCheckList( 

142 self, expr, test_string, expected_list, msg=None, verbose=True 

143 ): 

144 """ 

145 Convenience wrapper assert to test a parser element and input string, and assert that 

146 the resulting ``ParseResults.asList()`` is equal to the ``expected_list``. 

147 """ 

148 result = expr.parse_string(test_string, parse_all=True) 

149 if verbose: 

150 print(result.dump()) 

151 else: 

152 print(result.as_list()) 

153 self.assertParseResultsEquals(result, expected_list=expected_list, msg=msg) 

154 

155 def assertParseAndCheckDict( 

156 self, expr, test_string, expected_dict, msg=None, verbose=True 

157 ): 

158 """ 

159 Convenience wrapper assert to test a parser element and input string, and assert that 

160 the resulting ``ParseResults.asDict()`` is equal to the ``expected_dict``. 

161 """ 

162 result = expr.parse_string(test_string, parseAll=True) 

163 if verbose: 

164 print(result.dump()) 

165 else: 

166 print(result.as_list()) 

167 self.assertParseResultsEquals(result, expected_dict=expected_dict, msg=msg) 

168 

169 def assertRunTestResults( 

170 self, run_tests_report, expected_parse_results=None, msg=None 

171 ): 

172 """ 

173 Unit test assertion to evaluate output of ``ParserElement.runTests()``. If a list of 

174 list-dict tuples is given as the ``expected_parse_results`` argument, then these are zipped 

175 with the report tuples returned by ``runTests`` and evaluated using ``assertParseResultsEquals``. 

176 Finally, asserts that the overall ``runTests()`` success value is ``True``. 

177 

178 :param run_tests_report: tuple(bool, [tuple(str, ParseResults or Exception)]) returned from runTests 

179 :param expected_parse_results (optional): [tuple(str, list, dict, Exception)] 

180 """ 

181 run_test_success, run_test_results = run_tests_report 

182 

183 if expected_parse_results is not None: 

184 merged = [ 

185 (*rpt, expected) 

186 for rpt, expected in zip(run_test_results, expected_parse_results) 

187 ] 

188 for test_string, result, expected in merged: 

189 # expected should be a tuple containing a list and/or a dict or an exception, 

190 # and optional failure message string 

191 # an empty tuple will skip any result validation 

192 fail_msg = next( 

193 (exp for exp in expected if isinstance(exp, str)), None 

194 ) 

195 expected_exception = next( 

196 ( 

197 exp 

198 for exp in expected 

199 if isinstance(exp, type) and issubclass(exp, Exception) 

200 ), 

201 None, 

202 ) 

203 if expected_exception is not None: 

204 with self.assertRaises( 

205 expected_exception=expected_exception, msg=fail_msg or msg 

206 ): 

207 if isinstance(result, Exception): 

208 raise result 

209 else: 

210 expected_list = next( 

211 (exp for exp in expected if isinstance(exp, list)), None 

212 ) 

213 expected_dict = next( 

214 (exp for exp in expected if isinstance(exp, dict)), None 

215 ) 

216 if (expected_list, expected_dict) != (None, None): 

217 self.assertParseResultsEquals( 

218 result, 

219 expected_list=expected_list, 

220 expected_dict=expected_dict, 

221 msg=fail_msg or msg, 

222 ) 

223 else: 

224 # warning here maybe? 

225 print(f"no validation for {test_string!r}") 

226 

227 # do this last, in case some specific test results can be reported instead 

228 self.assertTrue( 

229 run_test_success, msg=msg if msg is not None else "failed runTests" 

230 ) 

231 

232 @contextmanager 

233 def assertRaisesParseException(self, exc_type=ParseException, msg=None): 

234 with self.assertRaises(exc_type, msg=msg): 

235 yield 

236 

237 @staticmethod 

238 def with_line_numbers( 

239 s: str, 

240 start_line: typing.Optional[int] = None, 

241 end_line: typing.Optional[int] = None, 

242 expand_tabs: bool = True, 

243 eol_mark: str = "|", 

244 mark_spaces: typing.Optional[str] = None, 

245 mark_control: typing.Optional[str] = None, 

246 ) -> str: 

247 """ 

248 Helpful method for debugging a parser - prints a string with line and column numbers. 

249 (Line and column numbers are 1-based.) 

250 

251 :param s: tuple(bool, str - string to be printed with line and column numbers 

252 :param start_line: int - (optional) starting line number in s to print (default=1) 

253 :param end_line: int - (optional) ending line number in s to print (default=len(s)) 

254 :param expand_tabs: bool - (optional) expand tabs to spaces, to match the pyparsing default 

255 :param eol_mark: str - (optional) string to mark the end of lines, helps visualize trailing spaces (default="|") 

256 :param mark_spaces: str - (optional) special character to display in place of spaces 

257 :param mark_control: str - (optional) convert non-printing control characters to a placeholding 

258 character; valid values: 

259 - "unicode" - replaces control chars with Unicode symbols, such as "␍" and "␊" 

260 - any single character string - replace control characters with given string 

261 - None (default) - string is displayed as-is 

262 

263 :return: str - input string with leading line numbers and column number headers 

264 """ 

265 if expand_tabs: 

266 s = s.expandtabs() 

267 if mark_control is not None: 

268 mark_control = typing.cast(str, mark_control) 

269 if mark_control == "unicode": 

270 transtable_map = { 

271 c: u for c, u in zip(range(0, 33), range(0x2400, 0x2433)) 

272 } 

273 transtable_map[127] = 0x2421 

274 tbl = str.maketrans(transtable_map) 

275 eol_mark = "" 

276 else: 

277 ord_mark_control = ord(mark_control) 

278 tbl = str.maketrans( 

279 {c: ord_mark_control for c in list(range(0, 32)) + [127]} 

280 ) 

281 s = s.translate(tbl) 

282 if mark_spaces is not None and mark_spaces != " ": 

283 if mark_spaces == "unicode": 

284 tbl = str.maketrans({9: 0x2409, 32: 0x2423}) 

285 s = s.translate(tbl) 

286 else: 

287 s = s.replace(" ", mark_spaces) 

288 if start_line is None: 

289 start_line = 1 

290 if end_line is None: 

291 end_line = len(s) 

292 end_line = min(end_line, len(s)) 

293 start_line = min(max(1, start_line), end_line) 

294 

295 if mark_control != "unicode": 

296 s_lines = s.splitlines()[start_line - 1 : end_line] 

297 else: 

298 s_lines = [line + "␊" for line in s.split("␊")[start_line - 1 : end_line]] 

299 if not s_lines: 

300 return "" 

301 

302 lineno_width = len(str(end_line)) 

303 max_line_len = max(len(line) for line in s_lines) 

304 lead = " " * (lineno_width + 1) 

305 if max_line_len >= 99: 

306 header0 = ( 

307 lead 

308 + "".join( 

309 f"{' ' * 99}{(i + 1) % 100}" 

310 for i in range(max(max_line_len // 100, 1)) 

311 ) 

312 + "\n" 

313 ) 

314 else: 

315 header0 = "" 

316 header1 = ( 

317 header0 

318 + lead 

319 + "".join(f" {(i + 1) % 10}" for i in range(-(-max_line_len // 10))) 

320 + "\n" 

321 ) 

322 header2 = lead + "1234567890" * (-(-max_line_len // 10)) + "\n" 

323 return ( 

324 header1 

325 + header2 

326 + "\n".join( 

327 f"{i:{lineno_width}d}:{line}{eol_mark}" 

328 for i, line in enumerate(s_lines, start=start_line) 

329 ) 

330 + "\n" 

331 )