3 from contextlib import contextmanager
17 namespace class for classes useful in writing unit tests
20 class reset_pyparsing_context:
22 Context manager to be used when writing unit tests that modify pyparsing config values:
24 - bounded recursion parsing
25 - default whitespace characters.
26 - default keyword characters
27 - literal string auto-conversion class
32 with reset_pyparsing_context():
33 # test that literals used to construct a grammar are automatically suppressed
34 ParserElement.inlineLiteralsUsing(Suppress)
36 term = Word(alphas) | Word(nums)
37 group = Group('(' + term[...] + ')')
39 # assert that the '()' characters are not included in the parsed tokens
40 self.assertParseAndCheckList(group, "(abc 123 def)", ['abc', '123', 'def'])
42 # after exiting context manager, literals are converted to Literal expressions again
46 self._save_context = {}
49 self._save_context["default_whitespace"] = ParserElement.DEFAULT_WHITE_CHARS
50 self._save_context["default_keyword_chars"] = Keyword.DEFAULT_KEYWORD_CHARS
53 "literal_string_class"
54 ] = ParserElement._literalStringClass
56 self._save_context["verbose_stacktrace"] = ParserElement.verbose_stacktrace
58 self._save_context["packrat_enabled"] = ParserElement._packratEnabled
59 if ParserElement._packratEnabled:
62 ] = ParserElement.packrat_cache.size
64 self._save_context["packrat_cache_size"] = None
65 self._save_context["packrat_parse"] = ParserElement._parse
68 ] = ParserElement._left_recursion_enabled
70 self._save_context["__diag__"] = {
71 name: getattr(__diag__, name) for name in __diag__._all_names
74 self._save_context["__compat__"] = {
75 "collect_all_And_tokens": __compat__.collect_all_And_tokens
81 # reset pyparsing global state
83 ParserElement.DEFAULT_WHITE_CHARS
84 != self._save_context["default_whitespace"]
86 ParserElement.set_default_whitespace_chars(
87 self._save_context["default_whitespace"]
90 ParserElement.verbose_stacktrace = self._save_context["verbose_stacktrace"]
92 Keyword.DEFAULT_KEYWORD_CHARS = self._save_context["default_keyword_chars"]
93 ParserElement.inlineLiteralsUsing(
94 self._save_context["literal_string_class"]
97 for name, value in self._save_context["__diag__"].items():
98 (__diag__.enable if value else __diag__.disable)(name)
100 ParserElement._packratEnabled = False
101 if self._save_context["packrat_enabled"]:
102 ParserElement.enable_packrat(self._save_context["packrat_cache_size"])
104 ParserElement._parse = self._save_context["packrat_parse"]
105 ParserElement._left_recursion_enabled = self._save_context[
109 __compat__.collect_all_And_tokens = self._save_context["__compat__"]
115 ret._save_context.update(self._save_context)
121 def __exit__(self, *args):
124 class TestParseResultsAsserts:
126 A mixin class to add parse results assertion methods to normal unittest.TestCase classes.
129 def assertParseResultsEquals(
130 self, result, expected_list=None, expected_dict=None, msg=None
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``.
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)
141 def assertParseAndCheckList(
142 self, expr, test_string, expected_list, msg=None, verbose=True
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``.
148 result = expr.parse_string(test_string, parse_all=True)
152 print(result.as_list())
153 self.assertParseResultsEquals(result, expected_list=expected_list, msg=msg)
155 def assertParseAndCheckDict(
156 self, expr, test_string, expected_dict, msg=None, verbose=True
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``.
162 result = expr.parse_string(test_string, parseAll=True)
166 print(result.as_list())
167 self.assertParseResultsEquals(result, expected_dict=expected_dict, msg=msg)
169 def assertRunTestResults(
170 self, run_tests_report, expected_parse_results=None, msg=None
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``.
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)]
181 run_test_success, run_test_results = run_tests_report
183 if expected_parse_results is not None:
186 for rpt, expected in zip(run_test_results, expected_parse_results)
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
193 (exp for exp in expected if isinstance(exp, str)), None
195 expected_exception = next(
199 if isinstance(exp, type) and issubclass(exp, Exception)
203 if expected_exception is not None:
204 with self.assertRaises(
205 expected_exception=expected_exception, msg=fail_msg or msg
207 if isinstance(result, Exception):
210 expected_list = next(
211 (exp for exp in expected if isinstance(exp, list)), None
213 expected_dict = next(
214 (exp for exp in expected if isinstance(exp, dict)), None
216 if (expected_list, expected_dict) != (None, None):
217 self.assertParseResultsEquals(
219 expected_list=expected_list,
220 expected_dict=expected_dict,
224 # warning here maybe?
225 print("no validation for {!r}".format(test_string))
227 # do this last, in case some specific test results can be reported instead
229 run_test_success, msg=msg if msg is not None else "failed runTests"
233 def assertRaisesParseException(self, exc_type=ParseException, msg=None):
234 with self.assertRaises(exc_type, msg=msg):
238 def with_line_numbers(
240 start_line: typing.Optional[int] = None,
241 end_line: typing.Optional[int] = None,
242 expand_tabs: bool = True,
244 mark_spaces: typing.Optional[str] = None,
245 mark_control: typing.Optional[str] = None,
248 Helpful method for debugging a parser - prints a string with line and column numbers.
249 (Line and column numbers are 1-based.)
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
263 :return: str - input string with leading line numbers and column number headers
267 if mark_control is not None:
268 if mark_control == "unicode":
270 {c: u for c, u in zip(range(0, 33), range(0x2400, 0x2433))}
276 {c: mark_control for c in list(range(0, 32)) + [127]}
279 if mark_spaces is not None and mark_spaces != " ":
280 if mark_spaces == "unicode":
281 tbl = str.maketrans({9: 0x2409, 32: 0x2423})
284 s = s.replace(" ", mark_spaces)
285 if start_line is None:
289 end_line = min(end_line, len(s))
290 start_line = min(max(1, start_line), end_line)
292 if mark_control != "unicode":
293 s_lines = s.splitlines()[start_line - 1 : end_line]
295 s_lines = [line + "␊" for line in s.split("␊")[start_line - 1 : end_line]]
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:
306 "{}{}".format(" " * 99, (i + 1) % 100)
307 for i in range(max(max_line_len // 100, 1))
317 " {}".format((i + 1) % 10)
318 for i in range(-(-max_line_len // 10))
322 header2 = lead + "1234567890" * (-(-max_line_len // 10)) + "\n"
327 "{:{}d}:{}{}".format(i, lineno_width, line, eol_mark)
328 for i, line in enumerate(s_lines, start=start_line)