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 with reset_pyparsing_context(): 

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

36 ParserElement.inlineLiteralsUsing(Suppress) 

37 

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

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

40 

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

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

43 

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

45 """ 

46 

47 def __init__(self): 

48 self._save_context = {} 

49 

50 def save(self): 

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

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

53 

54 self._save_context["literal_string_class"] = ( 

55 ParserElement._literalStringClass 

56 ) 

57 

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

59 

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

61 if ParserElement._packratEnabled: 

62 self._save_context["packrat_cache_size"] = ( 

63 ParserElement.packrat_cache.size 

64 ) 

65 else: 

66 self._save_context["packrat_cache_size"] = None 

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

68 self._save_context["recursion_enabled"] = ( 

69 ParserElement._left_recursion_enabled 

70 ) 

71 

72 self._save_context["__diag__"] = { 

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

74 } 

75 

76 self._save_context["__compat__"] = { 

77 "collect_all_And_tokens": __compat__.collect_all_And_tokens 

78 } 

79 

80 return self 

81 

82 def restore(self): 

83 # reset pyparsing global state 

84 if ( 

85 ParserElement.DEFAULT_WHITE_CHARS 

86 != self._save_context["default_whitespace"] 

87 ): 

88 ParserElement.set_default_whitespace_chars( 

89 self._save_context["default_whitespace"] 

90 ) 

91 

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

93 

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

95 ParserElement.inlineLiteralsUsing( 

96 self._save_context["literal_string_class"] 

97 ) 

98 

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

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

101 

102 ParserElement._packratEnabled = False 

103 if self._save_context["packrat_enabled"]: 

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

105 else: 

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

107 ParserElement._left_recursion_enabled = self._save_context[ 

108 "recursion_enabled" 

109 ] 

110 

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

112 

113 return self 

114 

115 def copy(self): 

116 ret = type(self)() 

117 ret._save_context.update(self._save_context) 

118 return ret 

119 

120 def __enter__(self): 

121 return self.save() 

122 

123 def __exit__(self, *args): 

124 self.restore() 

125 

126 class TestParseResultsAsserts: 

127 """ 

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

129 """ 

130 

131 def assertParseResultsEquals( 

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

133 ): 

134 """ 

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

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

137 """ 

138 if expected_list is not None: 

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

140 if expected_dict is not None: 

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

142 

143 def assertParseAndCheckList( 

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

145 ): 

146 """ 

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

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

149 """ 

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

151 if verbose: 

152 print(result.dump()) 

153 else: 

154 print(result.as_list()) 

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

156 

157 def assertParseAndCheckDict( 

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

159 ): 

160 """ 

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

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

163 """ 

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

165 if verbose: 

166 print(result.dump()) 

167 else: 

168 print(result.as_list()) 

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

170 

171 def assertRunTestResults( 

172 self, run_tests_report, expected_parse_results=None, msg=None 

173 ): 

174 """ 

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

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

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

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

179 

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

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

182 """ 

183 run_test_success, run_test_results = run_tests_report 

184 

185 if expected_parse_results is None: 

186 self.assertTrue( 

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

188 ) 

189 return 

190 

191 merged = [ 

192 (*rpt, expected) 

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

194 ] 

195 for test_string, result, expected in merged: 

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

197 # and optional failure message string 

198 # an empty tuple will skip any result validation 

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

200 expected_exception = next( 

201 ( 

202 exp 

203 for exp in expected 

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

205 ), 

206 None, 

207 ) 

208 if expected_exception is not None: 

209 with self.assertRaises( 

210 expected_exception=expected_exception, msg=fail_msg or msg 

211 ): 

212 if isinstance(result, Exception): 

213 raise result 

214 else: 

215 expected_list = next( 

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

217 ) 

218 expected_dict = next( 

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

220 ) 

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

222 self.assertParseResultsEquals( 

223 result, 

224 expected_list=expected_list, 

225 expected_dict=expected_dict, 

226 msg=fail_msg or msg, 

227 ) 

228 else: 

229 # warning here maybe? 

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

231 

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

233 self.assertTrue( 

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

235 ) 

236 

237 @contextmanager 

238 def assertRaisesParseException( 

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

240 ): 

241 if expected_msg is not None: 

242 if isinstance(expected_msg, str): 

243 expected_msg = re.escape(expected_msg) 

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

245 yield ctx 

246 

247 else: 

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

249 yield ctx 

250 

251 @staticmethod 

252 def with_line_numbers( 

253 s: str, 

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

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

256 expand_tabs: bool = True, 

257 eol_mark: str = "|", 

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

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

260 *, 

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

262 base_1: bool = True, 

263 ) -> str: 

264 """ 

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

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

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

268 

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

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

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

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

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

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

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

276 character; valid values: 

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

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

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

280 :param indent: str | int - (optional) string to indent with line and column numbers; if an int 

281 is passed, converted to " " * indent 

282 :param base_1: bool - (optional) whether to label string using base 1; if False, string will be 

283 labeled based at 0 (default=True) 

284 

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

286 """ 

287 if expand_tabs: 

288 s = s.expandtabs() 

289 if isinstance(indent, int): 

290 indent = " " * indent 

291 indent = indent.expandtabs() 

292 if mark_control is not None: 

293 mark_control = typing.cast(str, mark_control) 

294 if mark_control == "unicode": 

295 transtable_map = { 

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

297 } 

298 transtable_map[127] = 0x2421 

299 tbl = str.maketrans(transtable_map) 

300 eol_mark = "" 

301 else: 

302 ord_mark_control = ord(mark_control) 

303 tbl = str.maketrans( 

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

305 ) 

306 s = s.translate(tbl) 

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

308 if mark_spaces == "unicode": 

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

310 s = s.translate(tbl) 

311 else: 

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

313 if start_line is None: 

314 start_line = 0 

315 if end_line is None: 

316 end_line = len(s) 

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

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

319 

320 if mark_control != "unicode": 

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

322 else: 

323 s_lines = [ 

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

325 ] 

326 if not s_lines: 

327 return "" 

328 

329 lineno_width = len(str(end_line)) 

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

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

332 if max_line_len >= 99: 

333 header0 = ( 

334 lead 

335 + ("" if base_1 else " ") 

336 + "".join( 

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

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

339 ) 

340 + "\n" 

341 ) 

342 else: 

343 header0 = "" 

344 header1 = ( 

345 ("" if base_1 else " ") 

346 + lead 

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

348 + "\n" 

349 ) 

350 digits = "1234567890" 

351 header2 = ( 

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

353 ) 

354 return ( 

355 header1 

356 + header2 

357 + "\n".join( 

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

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

360 ) 

361 + "\n" 

362 )