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

118 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:23 +0000

1# testing.py 

2 

3from contextlib import contextmanager 

4import typing 

5 

6from .core import ( 

7 ParserElement, 

8 ParseException, 

9 Keyword, 

10 __diag__, 

11 __compat__, 

12) 

13 

14 

15class pyparsing_test: 

16 """ 

17 namespace class for classes useful in writing unit tests 

18 """ 

19 

20 class reset_pyparsing_context: 

21 """ 

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

23 - packrat parsing 

24 - bounded recursion parsing 

25 - default whitespace characters. 

26 - default keyword characters 

27 - literal string auto-conversion class 

28 - __diag__ settings 

29 

30 Example:: 

31 

32 with reset_pyparsing_context(): 

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

34 ParserElement.inlineLiteralsUsing(Suppress) 

35 

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

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

38 

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

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

41 

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

43 """ 

44 

45 def __init__(self): 

46 self._save_context = {} 

47 

48 def save(self): 

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

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

51 

52 self._save_context[ 

53 "literal_string_class" 

54 ] = ParserElement._literalStringClass 

55 

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

57 

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

59 if ParserElement._packratEnabled: 

60 self._save_context[ 

61 "packrat_cache_size" 

62 ] = ParserElement.packrat_cache.size 

63 else: 

64 self._save_context["packrat_cache_size"] = None 

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

66 self._save_context[ 

67 "recursion_enabled" 

68 ] = ParserElement._left_recursion_enabled 

69 

70 self._save_context["__diag__"] = { 

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

72 } 

73 

74 self._save_context["__compat__"] = { 

75 "collect_all_And_tokens": __compat__.collect_all_And_tokens 

76 } 

77 

78 return self 

79 

80 def restore(self): 

81 # reset pyparsing global state 

82 if ( 

83 ParserElement.DEFAULT_WHITE_CHARS 

84 != self._save_context["default_whitespace"] 

85 ): 

86 ParserElement.set_default_whitespace_chars( 

87 self._save_context["default_whitespace"] 

88 ) 

89 

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

91 

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

93 ParserElement.inlineLiteralsUsing( 

94 self._save_context["literal_string_class"] 

95 ) 

96 

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

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

99 

100 ParserElement._packratEnabled = False 

101 if self._save_context["packrat_enabled"]: 

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

103 else: 

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

105 ParserElement._left_recursion_enabled = self._save_context[ 

106 "recursion_enabled" 

107 ] 

108 

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

110 

111 return self 

112 

113 def copy(self): 

114 ret = type(self)() 

115 ret._save_context.update(self._save_context) 

116 return ret 

117 

118 def __enter__(self): 

119 return self.save() 

120 

121 def __exit__(self, *args): 

122 self.restore() 

123 

124 class TestParseResultsAsserts: 

125 """ 

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

127 """ 

128 

129 def assertParseResultsEquals( 

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

131 ): 

132 """ 

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

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

135 """ 

136 if expected_list is not None: 

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

138 if expected_dict is not None: 

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

140 

141 def assertParseAndCheckList( 

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

143 ): 

144 """ 

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

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

147 """ 

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

149 if verbose: 

150 print(result.dump()) 

151 else: 

152 print(result.as_list()) 

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

154 

155 def assertParseAndCheckDict( 

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

157 ): 

158 """ 

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

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

161 """ 

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

163 if verbose: 

164 print(result.dump()) 

165 else: 

166 print(result.as_list()) 

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

168 

169 def assertRunTestResults( 

170 self, run_tests_report, expected_parse_results=None, msg=None 

171 ): 

172 """ 

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

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

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

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

177 

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

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

180 """ 

181 run_test_success, run_test_results = run_tests_report 

182 

183 if expected_parse_results is not None: 

184 merged = [ 

185 (*rpt, expected) 

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

187 ] 

188 for test_string, result, expected in merged: 

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

190 # and optional failure message string 

191 # an empty tuple will skip any result validation 

192 fail_msg = next( 

193 (exp for exp in expected if isinstance(exp, str)), None 

194 ) 

195 expected_exception = next( 

196 ( 

197 exp 

198 for exp in expected 

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

200 ), 

201 None, 

202 ) 

203 if expected_exception is not None: 

204 with self.assertRaises( 

205 expected_exception=expected_exception, msg=fail_msg or msg 

206 ): 

207 if isinstance(result, Exception): 

208 raise result 

209 else: 

210 expected_list = next( 

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

212 ) 

213 expected_dict = next( 

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

215 ) 

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

217 self.assertParseResultsEquals( 

218 result, 

219 expected_list=expected_list, 

220 expected_dict=expected_dict, 

221 msg=fail_msg or msg, 

222 ) 

223 else: 

224 # warning here maybe? 

225 print("no validation for {!r}".format(test_string)) 

226 

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

228 self.assertTrue( 

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

230 ) 

231 

232 @contextmanager 

233 def assertRaisesParseException(self, exc_type=ParseException, msg=None): 

234 with self.assertRaises(exc_type, msg=msg): 

235 yield 

236 

237 @staticmethod 

238 def with_line_numbers( 

239 s: str, 

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

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

242 expand_tabs: bool = True, 

243 eol_mark: str = "|", 

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

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

246 ) -> str: 

247 """ 

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

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

250 

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

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

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

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

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

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

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

258 character; valid values: 

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

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

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

262 

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

264 """ 

265 if expand_tabs: 

266 s = s.expandtabs() 

267 if mark_control is not None: 

268 if mark_control == "unicode": 

269 tbl = str.maketrans( 

270 {c: u for c, u in zip(range(0, 33), range(0x2400, 0x2433))} 

271 | {127: 0x2421} 

272 ) 

273 eol_mark = "" 

274 else: 

275 tbl = str.maketrans( 

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

277 ) 

278 s = s.translate(tbl) 

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

280 if mark_spaces == "unicode": 

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

282 s = s.translate(tbl) 

283 else: 

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

285 if start_line is None: 

286 start_line = 1 

287 if end_line is None: 

288 end_line = len(s) 

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

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

291 

292 if mark_control != "unicode": 

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

294 else: 

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

296 if not s_lines: 

297 return "" 

298 

299 lineno_width = len(str(end_line)) 

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

301 lead = " " * (lineno_width + 1) 

302 if max_line_len >= 99: 

303 header0 = ( 

304 lead 

305 + "".join( 

306 "{}{}".format(" " * 99, (i + 1) % 100) 

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

308 ) 

309 + "\n" 

310 ) 

311 else: 

312 header0 = "" 

313 header1 = ( 

314 header0 

315 + lead 

316 + "".join( 

317 " {}".format((i + 1) % 10) 

318 for i in range(-(-max_line_len // 10)) 

319 ) 

320 + "\n" 

321 ) 

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

323 return ( 

324 header1 

325 + header2 

326 + "\n".join( 

327 "{:{}d}:{}{}".format(i, lineno_width, line, eol_mark) 

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

329 ) 

330 + "\n" 

331 )