Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/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

130 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 ) -> str: 

261 """ 

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

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

264 

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

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

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

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

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

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

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

272 character; valid values: 

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

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

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

276 

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

278 """ 

279 if expand_tabs: 

280 s = s.expandtabs() 

281 if mark_control is not None: 

282 mark_control = typing.cast(str, mark_control) 

283 if mark_control == "unicode": 

284 transtable_map = { 

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

286 } 

287 transtable_map[127] = 0x2421 

288 tbl = str.maketrans(transtable_map) 

289 eol_mark = "" 

290 else: 

291 ord_mark_control = ord(mark_control) 

292 tbl = str.maketrans( 

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

294 ) 

295 s = s.translate(tbl) 

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

297 if mark_spaces == "unicode": 

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

299 s = s.translate(tbl) 

300 else: 

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

302 if start_line is None: 

303 start_line = 1 

304 if end_line is None: 

305 end_line = len(s) 

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

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

308 

309 if mark_control != "unicode": 

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

311 else: 

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

313 if not s_lines: 

314 return "" 

315 

316 lineno_width = len(str(end_line)) 

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

318 lead = " " * (lineno_width + 1) 

319 if max_line_len >= 99: 

320 header0 = ( 

321 lead 

322 + "".join( 

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

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

325 ) 

326 + "\n" 

327 ) 

328 else: 

329 header0 = "" 

330 header1 = ( 

331 header0 

332 + lead 

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

334 + "\n" 

335 ) 

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

337 return ( 

338 header1 

339 + header2 

340 + "\n".join( 

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

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

343 ) 

344 + "\n" 

345 )