Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pyparsing/testing.py: 17%
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
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
1# testing.py
3from contextlib import contextmanager
4import re
5import typing
6import unittest
9from .core import (
10 ParserElement,
11 ParseException,
12 Keyword,
13 __diag__,
14 __compat__,
15)
16from . import core_builtin_exprs
19class pyparsing_test:
20 """
21 namespace class for classes useful in writing unit tests
22 """
24 class reset_pyparsing_context:
25 """
26 Context manager to be used when writing unit tests that modify pyparsing config values:
27 - packrat parsing
28 - bounded recursion parsing
29 - default whitespace characters
30 - default keyword characters
31 - literal string auto-conversion class
32 - ``__diag__`` settings
34 Example:
36 .. testcode::
38 ppt = pyparsing.pyparsing_test
40 class MyTestClass(ppt.TestParseResultsAsserts):
41 def test_literal(self):
42 with ppt.reset_pyparsing_context():
43 # test that literals used to construct
44 # a grammar are automatically suppressed
45 ParserElement.inline_literals_using(Suppress)
47 term = Word(alphas) | Word(nums)
48 group = Group('(' + term[...] + ')')
50 # assert that the '()' characters
51 # are not included in the parsed tokens
52 self.assertParseAndCheckList(
53 group,
54 "(abc 123 def)",
55 ['abc', '123', 'def']
56 )
58 # after exiting context manager, literals
59 # are converted to Literal expressions again
60 """
62 def __init__(self):
63 self._save_context = {}
65 def save(self):
66 self._save_context["default_whitespace"] = ParserElement.DEFAULT_WHITE_CHARS
67 self._save_context["default_keyword_chars"] = Keyword.DEFAULT_KEYWORD_CHARS
69 self._save_context["literal_string_class"] = (
70 ParserElement._literalStringClass
71 )
73 self._save_context["verbose_stacktrace"] = ParserElement.verbose_stacktrace
75 self._save_context["packrat_enabled"] = ParserElement._packratEnabled
76 if ParserElement._packratEnabled:
77 self._save_context["packrat_cache_size"] = (
78 ParserElement.packrat_cache.size
79 )
80 else:
81 self._save_context["packrat_cache_size"] = None
82 self._save_context["packrat_parse"] = ParserElement._parse
83 self._save_context["recursion_enabled"] = (
84 ParserElement._left_recursion_enabled
85 )
87 self._save_context["__diag__"] = {
88 name: getattr(__diag__, name) for name in __diag__._all_names
89 }
91 self._save_context["__compat__"] = {
92 "collect_all_And_tokens": __compat__.collect_all_And_tokens
93 }
95 return self
97 def restore(self):
98 # reset pyparsing global state
99 if (
100 ParserElement.DEFAULT_WHITE_CHARS
101 != self._save_context["default_whitespace"]
102 ):
103 ParserElement.set_default_whitespace_chars(
104 self._save_context["default_whitespace"]
105 )
107 ParserElement.verbose_stacktrace = self._save_context["verbose_stacktrace"]
109 Keyword.DEFAULT_KEYWORD_CHARS = self._save_context["default_keyword_chars"]
110 ParserElement.inline_literals_using(
111 self._save_context["literal_string_class"]
112 )
114 for name, value in self._save_context["__diag__"].items():
115 (__diag__.enable if value else __diag__.disable)(name)
117 ParserElement._packratEnabled = False
118 if self._save_context["packrat_enabled"]:
119 ParserElement.enable_packrat(self._save_context["packrat_cache_size"])
120 else:
121 ParserElement._parse = self._save_context["packrat_parse"]
122 ParserElement._left_recursion_enabled = self._save_context[
123 "recursion_enabled"
124 ]
126 # clear debug flags on all builtins
127 for expr in core_builtin_exprs:
128 expr.set_debug(False)
130 __compat__.collect_all_And_tokens = self._save_context["__compat__"]
132 return self
134 def copy(self):
135 ret = type(self)()
136 ret._save_context.update(self._save_context)
137 return ret
139 def __enter__(self):
140 return self.save()
142 def __exit__(self, *args):
143 self.restore()
145 class TestParseResultsAsserts(unittest.TestCase):
146 """
147 A mixin class to add parse results assertion methods to normal unittest.TestCase classes.
148 """
150 def assertParseResultsEquals(
151 self, result, expected_list=None, expected_dict=None, msg=None
152 ):
153 """
154 Unit test assertion to compare a :class:`ParseResults` object with an optional ``expected_list``,
155 and compare any defined results names with an optional ``expected_dict``.
156 """
157 if expected_list is not None:
158 self.assertEqual(expected_list, result.as_list(), msg=msg)
159 if expected_dict is not None:
160 self.assertEqual(expected_dict, result.as_dict(), msg=msg)
162 def assertParseAndCheckList(
163 self, expr, test_string, expected_list, msg=None, verbose=True
164 ):
165 """
166 Convenience wrapper assert to test a parser element and input string, and assert that
167 the resulting :meth:`ParseResults.as_list` is equal to the ``expected_list``.
168 """
169 result = expr.parse_string(test_string, parse_all=True)
170 if verbose:
171 print(result.dump())
172 else:
173 print(result.as_list())
174 self.assertParseResultsEquals(result, expected_list=expected_list, msg=msg)
176 def assertParseAndCheckDict(
177 self, expr, test_string, expected_dict, msg=None, verbose=True
178 ):
179 """
180 Convenience wrapper assert to test a parser element and input string, and assert that
181 the resulting :meth:`ParseResults.as_dict` is equal to the ``expected_dict``.
182 """
183 result = expr.parse_string(test_string, parse_all=True)
184 if verbose:
185 print(result.dump())
186 else:
187 print(result.as_list())
188 self.assertParseResultsEquals(result, expected_dict=expected_dict, msg=msg)
190 def assertRunTestResults(
191 self, run_tests_report, expected_parse_results=None, msg=None
192 ):
193 """
194 Unit test assertion to evaluate output of
195 :meth:`~ParserElement.run_tests`.
197 If a list of list-dict tuples is given as the
198 ``expected_parse_results`` argument, then these are zipped
199 with the report tuples returned by ``run_tests()``
200 and evaluated using :meth:`assertParseResultsEquals`.
201 Finally, asserts that the overall
202 `:meth:~ParserElement.run_tests` success value is ``True``.
204 :param run_tests_report: the return value from :meth:`ParserElement.run_tests`
205 :type run_tests_report: tuple[bool, list[tuple[str, ParseResults | Exception]]]
206 :param expected_parse_results: (optional)
207 :type expected_parse_results: list[tuple[str | list | dict | Exception, ...]]
208 """
209 run_test_success, run_test_results = run_tests_report
211 if expected_parse_results is None:
212 self.assertTrue(
213 run_test_success, msg=msg if msg is not None else "failed runTests"
214 )
215 return
217 merged = [
218 (*rpt, expected)
219 for rpt, expected in zip(run_test_results, expected_parse_results)
220 ]
221 for test_string, result, expected in merged:
222 # expected should be a tuple containing a list and/or a dict or an exception,
223 # and optional failure message string
224 # an empty tuple will skip any result validation
225 fail_msg = next((exp for exp in expected if isinstance(exp, str)), None)
226 expected_exception = next(
227 (
228 exp
229 for exp in expected
230 if isinstance(exp, type) and issubclass(exp, Exception)
231 ),
232 None,
233 )
234 if expected_exception is not None:
235 with self.assertRaises(
236 expected_exception=expected_exception, msg=fail_msg or msg
237 ):
238 if isinstance(result, Exception):
239 raise result
240 else:
241 expected_list = next(
242 (exp for exp in expected if isinstance(exp, list)), None
243 )
244 expected_dict = next(
245 (exp for exp in expected if isinstance(exp, dict)), None
246 )
247 if (expected_list, expected_dict) != (None, None):
248 self.assertParseResultsEquals(
249 result,
250 expected_list=expected_list,
251 expected_dict=expected_dict,
252 msg=fail_msg or msg,
253 )
254 else:
255 # warning here maybe?
256 print(f"no validation for {test_string!r}")
258 # do this last, in case some specific test results can be reported instead
259 self.assertTrue(
260 run_test_success, msg=msg if msg is not None else "failed runTests"
261 )
263 @contextmanager
264 def assertRaisesParseException(
265 self, exc_type=ParseException, expected_msg=None, msg=None
266 ):
267 if expected_msg is not None:
268 if isinstance(expected_msg, str):
269 expected_msg = re.escape(expected_msg)
270 with self.assertRaisesRegex(exc_type, expected_msg, msg=msg) as ctx:
271 yield ctx
273 else:
274 with self.assertRaises(exc_type, msg=msg) as ctx:
275 yield ctx
277 @staticmethod
278 def with_line_numbers(
279 s: str,
280 start_line: typing.Optional[int] = None,
281 end_line: typing.Optional[int] = None,
282 expand_tabs: bool = True,
283 eol_mark: str = "|",
284 mark_spaces: typing.Optional[str] = None,
285 mark_control: typing.Optional[str] = None,
286 *,
287 indent: typing.Union[str, int] = "",
288 base_1: bool = True,
289 ) -> str:
290 """
291 Helpful method for debugging a parser - prints a string with line and column numbers.
292 (Line and column numbers are 1-based by default - if debugging a parse action,
293 pass base_1=False, to correspond to the loc value passed to the parse action.)
295 :param s: string to be printed with line and column numbers
296 :param start_line: starting line number in s to print (default=1)
297 :param end_line: ending line number in s to print (default=len(s))
298 :param expand_tabs: expand tabs to spaces, to match the pyparsing default
299 :param eol_mark: string to mark the end of lines, helps visualize trailing spaces
300 :param mark_spaces: special character to display in place of spaces
301 :param mark_control: convert non-printing control characters to a placeholding
302 character; valid values:
304 - ``"unicode"`` - replaces control chars with Unicode symbols, such as "␍" and "␊"
305 - any single character string - replace control characters with given string
306 - ``None`` (default) - string is displayed as-is
309 :param indent: string to indent with line and column numbers; if an int
310 is passed, converted to ``" " * indent``
311 :param base_1: whether to label string using base 1; if False, string will be
312 labeled based at 0
314 :returns: input string with leading line numbers and column number headers
316 .. versionchanged:: 3.2.0
317 New ``indent`` and ``base_1`` arguments.
318 """
319 if expand_tabs:
320 s = s.expandtabs()
321 if isinstance(indent, int):
322 indent = " " * indent
323 indent = indent.expandtabs()
324 if mark_control is not None:
325 mark_control = typing.cast(str, mark_control)
326 if mark_control == "unicode":
327 transtable_map = {
328 c: u for c, u in zip(range(0, 33), range(0x2400, 0x2433))
329 }
330 transtable_map[127] = 0x2421
331 tbl = str.maketrans(transtable_map)
332 eol_mark = ""
333 else:
334 ord_mark_control = ord(mark_control)
335 tbl = str.maketrans(
336 {c: ord_mark_control for c in list(range(0, 32)) + [127]}
337 )
338 s = s.translate(tbl)
339 if mark_spaces is not None and mark_spaces != " ":
340 if mark_spaces == "unicode":
341 tbl = str.maketrans({9: 0x2409, 32: 0x2423})
342 s = s.translate(tbl)
343 else:
344 s = s.replace(" ", mark_spaces)
345 if start_line is None:
346 start_line = 0
347 if end_line is None:
348 end_line = len(s.splitlines())
349 end_line = min(end_line, len(s.splitlines()))
350 start_line = min(max(0, start_line), end_line)
352 if mark_control != "unicode":
353 s_lines = s.splitlines()[max(start_line - base_1, 0) : end_line]
354 else:
355 s_lines = [
356 line + "␊" for line in s.split("␊")[max(start_line - base_1, 0) : end_line]
357 ]
358 if not s_lines:
359 return ""
361 lineno_width = len(str(end_line))
362 max_line_len = max(len(line) for line in s_lines)
363 lead = indent + " " * (lineno_width + 1)
365 if max_line_len >= 99:
366 header0 = (
367 lead
368 + ("" if base_1 else " ")
369 + "".join(
370 f"{' ' * 99}{(i + 1) % 100}"
371 for i in range(max(max_line_len // 100, 1))
372 )
373 + "\n"
374 )
375 else:
376 header0 = ""
378 header1 = (
379 ("" if base_1 else " ")
380 + lead
381 + "".join(f" {(i + 1) % 10}" for i in range(-(-max_line_len // 10)))
382 + "\n"
383 )
384 digits = "1234567890"
385 header2 = (
386 lead + ("" if base_1 else "0") + digits * (-(-max_line_len // 10)) + "\n"
387 )
388 return (
389 header0
390 + header1
391 + header2
392 + "\n".join(
393 f"{indent}{i:{lineno_width}d}:{line}{eol_mark}"
394 for i, line in enumerate(s_lines, start=start_line + base_1)
395 )
396 + "\n"
397 )