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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

139 statements  

1# testing.py 

2 

3from contextlib import contextmanager 

4import re 

5import typing 

6import unittest 

7 

8 

9from .core import ( 

10 ParserElement, 

11 ParseException, 

12 Keyword, 

13 __diag__, 

14 __compat__, 

15) 

16from . import core_builtin_exprs 

17 

18 

19class pyparsing_test: 

20 """ 

21 namespace class for classes useful in writing unit tests 

22 """ 

23 

24 class reset_pyparsing_context: 

25 """ 

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

27 - packrat parsing 

28 - bounded recursion parsing 

29 - default whitespace characters 

30 - default keyword characters 

31 - literal string auto-conversion class 

32 - ``__diag__`` settings 

33 

34 Example: 

35 

36 .. testcode:: 

37 

38 ppt = pyparsing.pyparsing_test 

39 

40 class MyTestClass(ppt.TestParseResultsAsserts): 

41 def test_literal(self): 

42 with ppt.reset_pyparsing_context(): 

43 # test that literals used to construct 

44 # a grammar are automatically suppressed 

45 ParserElement.inline_literals_using(Suppress) 

46 

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

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

49 

50 # assert that the '()' characters 

51 # are not included in the parsed tokens 

52 self.assertParseAndCheckList( 

53 group, 

54 "(abc 123 def)", 

55 ['abc', '123', 'def'] 

56 ) 

57 

58 # after exiting context manager, literals 

59 # are converted to Literal expressions again 

60 """ 

61 

62 def __init__(self): 

63 self._save_context = {} 

64 

65 def save(self): 

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

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

68 

69 self._save_context["literal_string_class"] = ( 

70 ParserElement._literalStringClass 

71 ) 

72 

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

74 

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

76 if ParserElement._packratEnabled: 

77 self._save_context["packrat_cache_size"] = ( 

78 ParserElement.packrat_cache.size 

79 ) 

80 else: 

81 self._save_context["packrat_cache_size"] = None 

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

83 self._save_context["recursion_enabled"] = ( 

84 ParserElement._left_recursion_enabled 

85 ) 

86 

87 self._save_context["__diag__"] = { 

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

89 } 

90 

91 self._save_context["__compat__"] = { 

92 "collect_all_And_tokens": __compat__.collect_all_And_tokens 

93 } 

94 

95 return self 

96 

97 def restore(self): 

98 # reset pyparsing global state 

99 if ( 

100 ParserElement.DEFAULT_WHITE_CHARS 

101 != self._save_context["default_whitespace"] 

102 ): 

103 ParserElement.set_default_whitespace_chars( 

104 self._save_context["default_whitespace"] 

105 ) 

106 

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

108 

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

110 ParserElement.inline_literals_using( 

111 self._save_context["literal_string_class"] 

112 ) 

113 

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

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

116 

117 ParserElement._packratEnabled = False 

118 if self._save_context["packrat_enabled"]: 

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

120 else: 

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

122 ParserElement._left_recursion_enabled = self._save_context[ 

123 "recursion_enabled" 

124 ] 

125 

126 # clear debug flags on all builtins 

127 for expr in core_builtin_exprs: 

128 expr.set_debug(False) 

129 

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

131 

132 return self 

133 

134 def copy(self): 

135 ret = type(self)() 

136 ret._save_context.update(self._save_context) 

137 return ret 

138 

139 def __enter__(self): 

140 return self.save() 

141 

142 def __exit__(self, *args): 

143 self.restore() 

144 

145 class TestParseResultsAsserts(unittest.TestCase): 

146 """ 

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

148 """ 

149 

150 def assertParseResultsEquals( 

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

152 ): 

153 """ 

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

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

156 """ 

157 if expected_list is not None: 

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

159 if expected_dict is not None: 

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

161 

162 def assertParseAndCheckList( 

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

164 ): 

165 """ 

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

167 the resulting :meth:`ParseResults.as_list` is equal to the ``expected_list``. 

168 """ 

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

170 if verbose: 

171 print(result.dump()) 

172 else: 

173 print(result.as_list()) 

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

175 

176 def assertParseAndCheckDict( 

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

178 ): 

179 """ 

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

181 the resulting :meth:`ParseResults.as_dict` is equal to the ``expected_dict``. 

182 """ 

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

184 if verbose: 

185 print(result.dump()) 

186 else: 

187 print(result.as_list()) 

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

189 

190 def assertRunTestResults( 

191 self, run_tests_report, expected_parse_results=None, msg=None 

192 ): 

193 """ 

194 Unit test assertion to evaluate output of 

195 :meth:`~ParserElement.run_tests`. 

196 

197 If a list of list-dict tuples is given as the 

198 ``expected_parse_results`` argument, then these are zipped 

199 with the report tuples returned by ``run_tests()`` 

200 and evaluated using :meth:`assertParseResultsEquals`. 

201 Finally, asserts that the overall 

202 `:meth:~ParserElement.run_tests` success value is ``True``. 

203 

204 :param run_tests_report: the return value from :meth:`ParserElement.run_tests` 

205 :type run_tests_report: tuple[bool, list[tuple[str, ParseResults | Exception]]] 

206 :param expected_parse_results: (optional) 

207 :type expected_parse_results: list[tuple[str | list | dict | Exception, ...]] 

208 """ 

209 run_test_success, run_test_results = run_tests_report 

210 

211 if expected_parse_results is None: 

212 self.assertTrue( 

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

214 ) 

215 return 

216 

217 merged = [ 

218 (*rpt, expected) 

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

220 ] 

221 for test_string, result, expected in merged: 

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

223 # and optional failure message string 

224 # an empty tuple will skip any result validation 

225 fail_msg = next((exp for exp in expected if isinstance(exp, str)), None) 

226 expected_exception = next( 

227 ( 

228 exp 

229 for exp in expected 

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

231 ), 

232 None, 

233 ) 

234 if expected_exception is not None: 

235 with self.assertRaises( 

236 expected_exception=expected_exception, msg=fail_msg or msg 

237 ): 

238 if isinstance(result, Exception): 

239 raise result 

240 else: 

241 expected_list = next( 

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

243 ) 

244 expected_dict = next( 

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

246 ) 

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

248 self.assertParseResultsEquals( 

249 result, 

250 expected_list=expected_list, 

251 expected_dict=expected_dict, 

252 msg=fail_msg or msg, 

253 ) 

254 else: 

255 # warning here maybe? 

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

257 

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

259 self.assertTrue( 

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

261 ) 

262 

263 @contextmanager 

264 def assertRaisesParseException( 

265 self, exc_type=ParseException, expected_msg=None, msg=None 

266 ): 

267 if expected_msg is not None: 

268 if isinstance(expected_msg, str): 

269 expected_msg = re.escape(expected_msg) 

270 with self.assertRaisesRegex(exc_type, expected_msg, msg=msg) as ctx: 

271 yield ctx 

272 

273 else: 

274 with self.assertRaises(exc_type, msg=msg) as ctx: 

275 yield ctx 

276 

277 @staticmethod 

278 def with_line_numbers( 

279 s: str, 

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

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

282 expand_tabs: bool = True, 

283 eol_mark: str = "|", 

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

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

286 *, 

287 indent: typing.Union[str, int] = "", 

288 base_1: bool = True, 

289 ) -> str: 

290 """ 

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

292 (Line and column numbers are 1-based by default - if debugging a parse action, 

293 pass base_1=False, to correspond to the loc value passed to the parse action.) 

294 

295 :param s: string to be printed with line and column numbers 

296 :param start_line: starting line number in s to print (default=1) 

297 :param end_line: ending line number in s to print (default=len(s)) 

298 :param expand_tabs: expand tabs to spaces, to match the pyparsing default 

299 :param eol_mark: string to mark the end of lines, helps visualize trailing spaces 

300 :param mark_spaces: special character to display in place of spaces 

301 :param mark_control: convert non-printing control characters to a placeholding 

302 character; valid values: 

303 

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

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

306 - ``None`` (default) - string is displayed as-is 

307 

308 

309 :param indent: string to indent with line and column numbers; if an int 

310 is passed, converted to ``" " * indent`` 

311 :param base_1: whether to label string using base 1; if False, string will be 

312 labeled based at 0 

313 

314 :returns: input string with leading line numbers and column number headers 

315 

316 .. versionchanged:: 3.2.0 

317 New ``indent`` and ``base_1`` arguments. 

318 """ 

319 if expand_tabs: 

320 s = s.expandtabs() 

321 if isinstance(indent, int): 

322 indent = " " * indent 

323 indent = indent.expandtabs() 

324 if mark_control is not None: 

325 mark_control = typing.cast(str, mark_control) 

326 if mark_control == "unicode": 

327 transtable_map = { 

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

329 } 

330 transtable_map[127] = 0x2421 

331 tbl = str.maketrans(transtable_map) 

332 eol_mark = "" 

333 else: 

334 ord_mark_control = ord(mark_control) 

335 tbl = str.maketrans( 

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

337 ) 

338 s = s.translate(tbl) 

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

340 if mark_spaces == "unicode": 

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

342 s = s.translate(tbl) 

343 else: 

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

345 if start_line is None: 

346 start_line = 0 

347 if end_line is None: 

348 end_line = len(s.splitlines()) 

349 end_line = min(end_line, len(s.splitlines())) 

350 start_line = min(max(0, start_line), end_line) 

351 

352 if mark_control != "unicode": 

353 s_lines = s.splitlines()[max(start_line - base_1, 0) : end_line] 

354 else: 

355 s_lines = [ 

356 line + "␊" for line in s.split("␊")[max(start_line - base_1, 0) : end_line] 

357 ] 

358 if not s_lines: 

359 return "" 

360 

361 lineno_width = len(str(end_line)) 

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

363 lead = indent + " " * (lineno_width + 1) 

364 

365 if max_line_len >= 99: 

366 header0 = ( 

367 lead 

368 + ("" if base_1 else " ") 

369 + "".join( 

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

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

372 ) 

373 + "\n" 

374 ) 

375 else: 

376 header0 = "" 

377 

378 header1 = ( 

379 ("" if base_1 else " ") 

380 + lead 

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

382 + "\n" 

383 ) 

384 digits = "1234567890" 

385 header2 = ( 

386 lead + ("" if base_1 else "0") + digits * (-(-max_line_len // 10)) + "\n" 

387 ) 

388 return ( 

389 header0 

390 + header1 

391 + header2 

392 + "\n".join( 

393 f"{indent}{i:{lineno_width}d}:{line}{eol_mark}" 

394 for i, line in enumerate(s_lines, start=start_line + base_1) 

395 ) 

396 + "\n" 

397 )