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

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

136 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) 

16 

17 

18class pyparsing_test: 

19 """ 

20 namespace class for classes useful in writing unit tests 

21 """ 

22 

23 class reset_pyparsing_context: 

24 """ 

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

26 - packrat parsing 

27 - bounded recursion parsing 

28 - default whitespace characters 

29 - default keyword characters 

30 - literal string auto-conversion class 

31 - ``__diag__`` settings 

32 

33 Example: 

34 

35 .. testcode:: 

36 

37 ppt = pyparsing.pyparsing_test 

38 

39 class MyTestClass(ppt.TestParseResultsAsserts): 

40 def test_literal(self): 

41 with ppt.reset_pyparsing_context(): 

42 # test that literals used to construct 

43 # a grammar are automatically suppressed 

44 ParserElement.inline_literals_using(Suppress) 

45 

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

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

48 

49 # assert that the '()' characters 

50 # are not included in the parsed tokens 

51 self.assertParseAndCheckList( 

52 group, 

53 "(abc 123 def)", 

54 ['abc', '123', 'def'] 

55 ) 

56 

57 # after exiting context manager, literals 

58 # are converted to Literal expressions again 

59 """ 

60 

61 def __init__(self): 

62 self._save_context = {} 

63 

64 def save(self): 

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

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

67 

68 self._save_context["literal_string_class"] = ( 

69 ParserElement._literalStringClass 

70 ) 

71 

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

73 

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

75 if ParserElement._packratEnabled: 

76 self._save_context["packrat_cache_size"] = ( 

77 ParserElement.packrat_cache.size 

78 ) 

79 else: 

80 self._save_context["packrat_cache_size"] = None 

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

82 self._save_context["recursion_enabled"] = ( 

83 ParserElement._left_recursion_enabled 

84 ) 

85 

86 self._save_context["__diag__"] = { 

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

88 } 

89 

90 self._save_context["__compat__"] = { 

91 "collect_all_And_tokens": __compat__.collect_all_And_tokens 

92 } 

93 

94 return self 

95 

96 def restore(self): 

97 # reset pyparsing global state 

98 if ( 

99 ParserElement.DEFAULT_WHITE_CHARS 

100 != self._save_context["default_whitespace"] 

101 ): 

102 ParserElement.set_default_whitespace_chars( 

103 self._save_context["default_whitespace"] 

104 ) 

105 

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

107 

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

109 ParserElement.inline_literals_using( 

110 self._save_context["literal_string_class"] 

111 ) 

112 

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

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

115 

116 ParserElement._packratEnabled = False 

117 if self._save_context["packrat_enabled"]: 

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

119 else: 

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

121 ParserElement._left_recursion_enabled = self._save_context[ 

122 "recursion_enabled" 

123 ] 

124 

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

126 

127 return self 

128 

129 def copy(self): 

130 ret = type(self)() 

131 ret._save_context.update(self._save_context) 

132 return ret 

133 

134 def __enter__(self): 

135 return self.save() 

136 

137 def __exit__(self, *args): 

138 self.restore() 

139 

140 class TestParseResultsAsserts(unittest.TestCase): 

141 """ 

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

143 """ 

144 

145 def assertParseResultsEquals( 

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

147 ): 

148 """ 

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

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

151 """ 

152 if expected_list is not None: 

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

154 if expected_dict is not None: 

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

156 

157 def assertParseAndCheckList( 

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

159 ): 

160 """ 

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

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

163 """ 

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

165 if verbose: 

166 print(result.dump()) 

167 else: 

168 print(result.as_list()) 

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

170 

171 def assertParseAndCheckDict( 

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

173 ): 

174 """ 

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

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

177 """ 

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

179 if verbose: 

180 print(result.dump()) 

181 else: 

182 print(result.as_list()) 

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

184 

185 def assertRunTestResults( 

186 self, run_tests_report, expected_parse_results=None, msg=None 

187 ): 

188 """ 

189 Unit test assertion to evaluate output of 

190 :meth:`~ParserElement.run_tests`. 

191 

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

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

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

195 and evaluated using :meth:`assertParseResultsEquals`. 

196 Finally, asserts that the overall 

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

198 

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

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

201 :param expected_parse_results: (optional) 

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

203 """ 

204 run_test_success, run_test_results = run_tests_report 

205 

206 if expected_parse_results is None: 

207 self.assertTrue( 

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

209 ) 

210 return 

211 

212 merged = [ 

213 (*rpt, expected) 

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

215 ] 

216 for test_string, result, expected in merged: 

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

218 # and optional failure message string 

219 # an empty tuple will skip any result validation 

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

221 expected_exception = next( 

222 ( 

223 exp 

224 for exp in expected 

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

226 ), 

227 None, 

228 ) 

229 if expected_exception is not None: 

230 with self.assertRaises( 

231 expected_exception=expected_exception, msg=fail_msg or msg 

232 ): 

233 if isinstance(result, Exception): 

234 raise result 

235 else: 

236 expected_list = next( 

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

238 ) 

239 expected_dict = next( 

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

241 ) 

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

243 self.assertParseResultsEquals( 

244 result, 

245 expected_list=expected_list, 

246 expected_dict=expected_dict, 

247 msg=fail_msg or msg, 

248 ) 

249 else: 

250 # warning here maybe? 

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

252 

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

254 self.assertTrue( 

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

256 ) 

257 

258 @contextmanager 

259 def assertRaisesParseException( 

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

261 ): 

262 if expected_msg is not None: 

263 if isinstance(expected_msg, str): 

264 expected_msg = re.escape(expected_msg) 

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

266 yield ctx 

267 

268 else: 

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

270 yield ctx 

271 

272 @staticmethod 

273 def with_line_numbers( 

274 s: str, 

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

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

277 expand_tabs: bool = True, 

278 eol_mark: str = "|", 

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

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

281 *, 

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

283 base_1: bool = True, 

284 ) -> str: 

285 """ 

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

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

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

289 

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

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

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

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

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

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

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

297 character; valid values: 

298 

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

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

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

302 

303 

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

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

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

307 labeled based at 0 

308 

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

310 

311 .. versionchanged:: 3.2.0 

312 New ``indent`` and ``base_1`` arguments. 

313 """ 

314 if expand_tabs: 

315 s = s.expandtabs() 

316 if isinstance(indent, int): 

317 indent = " " * indent 

318 indent = indent.expandtabs() 

319 if mark_control is not None: 

320 mark_control = typing.cast(str, mark_control) 

321 if mark_control == "unicode": 

322 transtable_map = { 

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

324 } 

325 transtable_map[127] = 0x2421 

326 tbl = str.maketrans(transtable_map) 

327 eol_mark = "" 

328 else: 

329 ord_mark_control = ord(mark_control) 

330 tbl = str.maketrans( 

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

332 ) 

333 s = s.translate(tbl) 

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

335 if mark_spaces == "unicode": 

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

337 s = s.translate(tbl) 

338 else: 

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

340 if start_line is None: 

341 start_line = 0 

342 if end_line is None: 

343 end_line = len(s) 

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

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

346 

347 if mark_control != "unicode": 

348 s_lines = s.splitlines()[start_line - base_1 : end_line] 

349 else: 

350 s_lines = [ 

351 line + "␊" for line in s.split("␊")[start_line - base_1 : end_line] 

352 ] 

353 if not s_lines: 

354 return "" 

355 

356 lineno_width = len(str(end_line)) 

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

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

359 if max_line_len >= 99: 

360 header0 = ( 

361 lead 

362 + ("" if base_1 else " ") 

363 + "".join( 

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

365 for i in range(1 if base_1 else 0, max(max_line_len // 100, 1)) 

366 ) 

367 + "\n" 

368 ) 

369 else: 

370 header0 = "" 

371 header1 = ( 

372 ("" if base_1 else " ") 

373 + lead 

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

375 + "\n" 

376 ) 

377 digits = "1234567890" 

378 header2 = ( 

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

380 ) 

381 return ( 

382 header1 

383 + header2 

384 + "\n".join( 

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

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

387 ) 

388 + "\n" 

389 )