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

135 statements  

1# testing.py 

2 

3from contextlib import contextmanager 

4import re 

5import typing 

6 

7 

8from .core import ( 

9 ParserElement, 

10 ParseException, 

11 Keyword, 

12 __diag__, 

13 __compat__, 

14) 

15 

16 

17class pyparsing_test: 

18 """ 

19 namespace class for classes useful in writing unit tests 

20 """ 

21 

22 class reset_pyparsing_context: 

23 """ 

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

25 - packrat parsing 

26 - bounded recursion parsing 

27 - default whitespace characters 

28 - default keyword characters 

29 - literal string auto-conversion class 

30 - ``__diag__`` settings 

31 

32 Example: 

33 

34 .. testcode:: 

35 

36 ppt = pyparsing.pyparsing_test 

37 

38 class MyTestClass(ppt.TestParseResultsAsserts): 

39 def test_literal(self): 

40 with ppt.reset_pyparsing_context(): 

41 # test that literals used to construct 

42 # a grammar are automatically suppressed 

43 ParserElement.inline_literals_using(Suppress) 

44 

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

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

47 

48 # assert that the '()' characters 

49 # are not included in the parsed tokens 

50 self.assertParseAndCheckList( 

51 group, 

52 "(abc 123 def)", 

53 ['abc', '123', 'def'] 

54 ) 

55 

56 # after exiting context manager, literals 

57 # are converted to Literal expressions again 

58 """ 

59 

60 def __init__(self): 

61 self._save_context = {} 

62 

63 def save(self): 

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

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

66 

67 self._save_context["literal_string_class"] = ( 

68 ParserElement._literalStringClass 

69 ) 

70 

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

72 

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

74 if ParserElement._packratEnabled: 

75 self._save_context["packrat_cache_size"] = ( 

76 ParserElement.packrat_cache.size 

77 ) 

78 else: 

79 self._save_context["packrat_cache_size"] = None 

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

81 self._save_context["recursion_enabled"] = ( 

82 ParserElement._left_recursion_enabled 

83 ) 

84 

85 self._save_context["__diag__"] = { 

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

87 } 

88 

89 self._save_context["__compat__"] = { 

90 "collect_all_And_tokens": __compat__.collect_all_And_tokens 

91 } 

92 

93 return self 

94 

95 def restore(self): 

96 # reset pyparsing global state 

97 if ( 

98 ParserElement.DEFAULT_WHITE_CHARS 

99 != self._save_context["default_whitespace"] 

100 ): 

101 ParserElement.set_default_whitespace_chars( 

102 self._save_context["default_whitespace"] 

103 ) 

104 

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

106 

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

108 ParserElement.inlineLiteralsUsing( 

109 self._save_context["literal_string_class"] 

110 ) 

111 

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

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

114 

115 ParserElement._packratEnabled = False 

116 if self._save_context["packrat_enabled"]: 

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

118 else: 

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

120 ParserElement._left_recursion_enabled = self._save_context[ 

121 "recursion_enabled" 

122 ] 

123 

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

125 

126 return self 

127 

128 def copy(self): 

129 ret = type(self)() 

130 ret._save_context.update(self._save_context) 

131 return ret 

132 

133 def __enter__(self): 

134 return self.save() 

135 

136 def __exit__(self, *args): 

137 self.restore() 

138 

139 class TestParseResultsAsserts: 

140 """ 

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

142 """ 

143 

144 def assertParseResultsEquals( 

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

146 ): 

147 """ 

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

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

150 """ 

151 if expected_list is not None: 

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

153 if expected_dict is not None: 

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

155 

156 def assertParseAndCheckList( 

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

158 ): 

159 """ 

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

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

162 """ 

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

164 if verbose: 

165 print(result.dump()) 

166 else: 

167 print(result.as_list()) 

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

169 

170 def assertParseAndCheckDict( 

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

172 ): 

173 """ 

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

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

176 """ 

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

178 if verbose: 

179 print(result.dump()) 

180 else: 

181 print(result.as_list()) 

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

183 

184 def assertRunTestResults( 

185 self, run_tests_report, expected_parse_results=None, msg=None 

186 ): 

187 """ 

188 Unit test assertion to evaluate output of 

189 :meth:`~ParserElement.run_tests`. 

190 

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

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

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

194 and evaluated using :meth:`assertParseResultsEquals`. 

195 Finally, asserts that the overall 

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

197 

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

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

200 :param expected_parse_results: (optional) 

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

202 """ 

203 run_test_success, run_test_results = run_tests_report 

204 

205 if expected_parse_results is None: 

206 self.assertTrue( 

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

208 ) 

209 return 

210 

211 merged = [ 

212 (*rpt, expected) 

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

214 ] 

215 for test_string, result, expected in merged: 

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

217 # and optional failure message string 

218 # an empty tuple will skip any result validation 

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

220 expected_exception = next( 

221 ( 

222 exp 

223 for exp in expected 

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

225 ), 

226 None, 

227 ) 

228 if expected_exception is not None: 

229 with self.assertRaises( 

230 expected_exception=expected_exception, msg=fail_msg or msg 

231 ): 

232 if isinstance(result, Exception): 

233 raise result 

234 else: 

235 expected_list = next( 

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

237 ) 

238 expected_dict = next( 

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

240 ) 

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

242 self.assertParseResultsEquals( 

243 result, 

244 expected_list=expected_list, 

245 expected_dict=expected_dict, 

246 msg=fail_msg or msg, 

247 ) 

248 else: 

249 # warning here maybe? 

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

251 

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

253 self.assertTrue( 

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

255 ) 

256 

257 @contextmanager 

258 def assertRaisesParseException( 

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

260 ): 

261 if expected_msg is not None: 

262 if isinstance(expected_msg, str): 

263 expected_msg = re.escape(expected_msg) 

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

265 yield ctx 

266 

267 else: 

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

269 yield ctx 

270 

271 @staticmethod 

272 def with_line_numbers( 

273 s: str, 

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

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

276 expand_tabs: bool = True, 

277 eol_mark: str = "|", 

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

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

280 *, 

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

282 base_1: bool = True, 

283 ) -> str: 

284 """ 

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

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

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

288 

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

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

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

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

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

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

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

296 character; valid values: 

297  

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

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

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

301 

302 

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

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

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

306 labeled based at 0 

307 

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

309 

310 .. versionchanged:: 3.2.0 

311 New ``indent`` and ``base_1`` arguments. 

312 """ 

313 if expand_tabs: 

314 s = s.expandtabs() 

315 if isinstance(indent, int): 

316 indent = " " * indent 

317 indent = indent.expandtabs() 

318 if mark_control is not None: 

319 mark_control = typing.cast(str, mark_control) 

320 if mark_control == "unicode": 

321 transtable_map = { 

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

323 } 

324 transtable_map[127] = 0x2421 

325 tbl = str.maketrans(transtable_map) 

326 eol_mark = "" 

327 else: 

328 ord_mark_control = ord(mark_control) 

329 tbl = str.maketrans( 

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

331 ) 

332 s = s.translate(tbl) 

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

334 if mark_spaces == "unicode": 

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

336 s = s.translate(tbl) 

337 else: 

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

339 if start_line is None: 

340 start_line = 0 

341 if end_line is None: 

342 end_line = len(s) 

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

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

345 

346 if mark_control != "unicode": 

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

348 else: 

349 s_lines = [ 

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

351 ] 

352 if not s_lines: 

353 return "" 

354 

355 lineno_width = len(str(end_line)) 

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

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

358 if max_line_len >= 99: 

359 header0 = ( 

360 lead 

361 + ("" if base_1 else " ") 

362 + "".join( 

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

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

365 ) 

366 + "\n" 

367 ) 

368 else: 

369 header0 = "" 

370 header1 = ( 

371 ("" if base_1 else " ") 

372 + lead 

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

374 + "\n" 

375 ) 

376 digits = "1234567890" 

377 header2 = ( 

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

379 ) 

380 return ( 

381 header1 

382 + header2 

383 + "\n".join( 

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

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

386 ) 

387 + "\n" 

388 )