Aluode/PerceptionLabPortable
0
1# testing.py2 3from contextlib import contextmanager4import re5import typing6import unittest7 8 9from .core import (10 ParserElement,11 ParseException,12 Keyword,13 __diag__,14 __compat__,15)16from . import core_builtin_exprs17 18 19class pyparsing_test:20 """21 namespace class for classes useful in writing unit tests22 """23 24 class reset_pyparsing_context:25 """26 Context manager to be used when writing unit tests that modify pyparsing config values:27 - packrat parsing28 - bounded recursion parsing29 - default whitespace characters30 - default keyword characters31 - literal string auto-conversion class32 - ``__diag__`` settings33 34 Example:35 36 .. testcode::37 38 ppt = pyparsing.pyparsing_test39 40 class MyTestClass(ppt.TestParseResultsAsserts):41 def test_literal(self):42 with ppt.reset_pyparsing_context():43 # test that literals used to construct44 # a grammar are automatically suppressed45 ParserElement.inline_literals_using(Suppress)46 47 term = Word(alphas) | Word(nums)48 group = Group('(' + term[...] + ')')49 50 # assert that the '()' characters51 # are not included in the parsed tokens52 self.assertParseAndCheckList(53 group,54 "(abc 123 def)",55 ['abc', '123', 'def']56 )57 58 # after exiting context manager, literals59 # are converted to Literal expressions again60 """61 62 def __init__(self):63 self._save_context = {}64 65 def save(self):66 self._save_context["default_whitespace"] = ParserElement.DEFAULT_WHITE_CHARS67 self._save_context["default_keyword_chars"] = Keyword.DEFAULT_KEYWORD_CHARS68 69 self._save_context["literal_string_class"] = (70 ParserElement._literalStringClass71 )72 73 self._save_context["verbose_stacktrace"] = ParserElement.verbose_stacktrace74 75 self._save_context["packrat_enabled"] = ParserElement._packratEnabled76 if ParserElement._packratEnabled:77 self._save_context["packrat_cache_size"] = (78 ParserElement.packrat_cache.size79 )80 else:81 self._save_context["packrat_cache_size"] = None82 self._save_context["packrat_parse"] = ParserElement._parse83 self._save_context["recursion_enabled"] = (84 ParserElement._left_recursion_enabled85 )86 87 self._save_context["__diag__"] = {88 name: getattr(__diag__, name) for name in __diag__._all_names89 }90 91 self._save_context["__compat__"] = {92 "collect_all_And_tokens": __compat__.collect_all_And_tokens93 }94 95 return self96 97 def restore(self):98 # reset pyparsing global state99 if (100 ParserElement.DEFAULT_WHITE_CHARS101 != self._save_context["default_whitespace"]102 ):103 ParserElement.set_default_whitespace_chars(104 self._save_context["default_whitespace"]105 )106 107 ParserElement.verbose_stacktrace = self._save_context["verbose_stacktrace"]108 109 Keyword.DEFAULT_KEYWORD_CHARS = self._save_context["default_keyword_chars"]110 ParserElement.inline_literals_using(111 self._save_context["literal_string_class"]112 )113 114 for name, value in self._save_context["__diag__"].items():115 (__diag__.enable if value else __diag__.disable)(name)116 117 ParserElement._packratEnabled = False118 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 ]125 126 # clear debug flags on all builtins127 for expr in core_builtin_exprs:128 expr.set_debug(False)129 130 __compat__.collect_all_And_tokens = self._save_context["__compat__"]131 132 return self133 134 def copy(self):135 ret = type(self)()136 ret._save_context.update(self._save_context)137 return ret138 139 def __enter__(self):140 return self.save()141 142 def __exit__(self, *args):143 self.restore()144 145 class TestParseResultsAsserts(unittest.TestCase):146 """147 A mixin class to add parse results assertion methods to normal unittest.TestCase classes.148 """149 150 def assertParseResultsEquals(151 self, result, expected_list=None, expected_dict=None, msg=None152 ):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)161 162 def assertParseAndCheckList(163 self, expr, test_string, expected_list, msg=None, verbose=True164 ):165 """166 Convenience wrapper assert to test a parser element and input string, and assert that167 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)175 176 def assertParseAndCheckDict(177 self, expr, test_string, expected_dict, msg=None, verbose=True178 ):179 """180 Convenience wrapper assert to test a parser element and input string, and assert that181 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)189 190 def assertRunTestResults(191 self, run_tests_report, expected_parse_results=None, msg=None192 ):193 """194 Unit test assertion to evaluate output of195 :meth:`~ParserElement.run_tests`.196 197 If a list of list-dict tuples is given as the198 ``expected_parse_results`` argument, then these are zipped199 with the report tuples returned by ``run_tests()``200 and evaluated using :meth:`assertParseResultsEquals`.201 Finally, asserts that the overall202 `:meth:~ParserElement.run_tests` success value is ``True``.203 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_report210 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 return216 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 string224 # an empty tuple will skip any result validation225 fail_msg = next((exp for exp in expected if isinstance(exp, str)), None)226 expected_exception = next(227 (228 exp229 for exp in expected230 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 msg237 ):238 if isinstance(result, Exception):239 raise result240 else:241 expected_list = next(242 (exp for exp in expected if isinstance(exp, list)), None243 )244 expected_dict = next(245 (exp for exp in expected if isinstance(exp, dict)), None246 )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}")257 258 # do this last, in case some specific test results can be reported instead259 self.assertTrue(260 run_test_success, msg=msg if msg is not None else "failed runTests"261 )262 263 @contextmanager264 def assertRaisesParseException(265 self, exc_type=ParseException, expected_msg=None, msg=None266 ):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 ctx272 273 else:274 with self.assertRaises(exc_type, msg=msg) as ctx:275 yield ctx276 277 @staticmethod278 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.)294 295 :param s: string to be printed with line and column numbers296 :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 default299 :param eol_mark: string to mark the end of lines, helps visualize trailing spaces300 :param mark_spaces: special character to display in place of spaces301 :param mark_control: convert non-printing control characters to a placeholding302 character; valid values:303 304 - ``"unicode"`` - replaces control chars with Unicode symbols, such as "␍" and "␊"305 - any single character string - replace control characters with given string306 - ``None`` (default) - string is displayed as-is307 308 309 :param indent: string to indent with line and column numbers; if an int310 is passed, converted to ``" " * indent``311 :param base_1: whether to label string using base 1; if False, string will be312 labeled based at 0313 314 :returns: input string with leading line numbers and column number headers315 316 .. versionchanged:: 3.2.0317 New ``indent`` and ``base_1`` arguments.318 """319 if expand_tabs:320 s = s.expandtabs()321 if isinstance(indent, int):322 indent = " " * indent323 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] = 0x2421331 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 = 0347 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)351 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 + "␊"357 for line in s.split("␊")[max(start_line - base_1, 0) : end_line]358 ]359 if not s_lines:360 return ""361 362 lineno_width = len(str(end_line))363 max_line_len = max(len(line) for line in s_lines)364 lead = indent + " " * (lineno_width + 1)365 366 if max_line_len >= 99:367 header0 = (368 lead369 + ("" if base_1 else " ")370 + "".join(371 f"{' ' * 99}{(i + 1) % 100}"372 for i in range(max(max_line_len // 100, 1))373 )374 + "\n"375 )376 else:377 header0 = ""378 379 header1 = (380 ("" if base_1 else " ")381 + lead382 + "".join(f" {(i + 1) % 10}" for i in range(-(-max_line_len // 10)))383 + "\n"384 )385 digits = "1234567890"386 header2 = (387 lead + ("" if base_1 else "0") + digits * (-(-max_line_len // 10)) + "\n"388 )389 return (390 header0391 + header1392 + header2393 + "\n".join(394 f"{indent}{i:{lineno_width}d}:{line}{eol_mark}"395 for i, line in enumerate(s_lines, start=start_line + base_1)396 )397 + "\n"398 )399 