12093 lines
425 KiB
Python
12093 lines
425 KiB
Python
#
|
||
# test_unit.py
|
||
#
|
||
# Unit tests for pyparsing module
|
||
#
|
||
# Copyright 2002-2021, Paul McGuire
|
||
#
|
||
#
|
||
import collections
|
||
import contextlib
|
||
import datetime
|
||
import random
|
||
import re
|
||
import shlex
|
||
import sys
|
||
import sysconfig
|
||
import warnings
|
||
from types import SimpleNamespace
|
||
from io import StringIO
|
||
from textwrap import dedent
|
||
from typing import Any
|
||
import unittest
|
||
from unittest.mock import patch, mock_open
|
||
import subprocess
|
||
|
||
import pyparsing as pp
|
||
from examples.jsonParser import jsonObject
|
||
from pyparsing import (
|
||
ParserElement,
|
||
ParseException,
|
||
ParseFatalException,
|
||
PyparsingDeprecationWarning,
|
||
)
|
||
from tests.json_parser_tests import test1, test2, test3, test4, test5
|
||
import platform
|
||
|
||
python_full_version = sys.version_info
|
||
python_version = python_full_version[:2]
|
||
|
||
ppc = pp.pyparsing_common
|
||
ppt = pp.pyparsing_test
|
||
|
||
# see which Python implementation we are running
|
||
python_impl = platform.python_implementation()
|
||
CPYTHON_ENV = python_impl == "CPython"
|
||
IRON_PYTHON_ENV = python_impl == "IronPython"
|
||
JYTHON_ENV = python_impl == "Jython"
|
||
PYPY_ENV = python_impl == "PyPy"
|
||
|
||
# global flags for Python config settings
|
||
_config_vars = sysconfig.get_config_vars()
|
||
_config_args = set(shlex.split(_config_vars.get("CONFIG_ARGS", "")))
|
||
PYTHON_JIT_ENABLED = "--enable-experimental-jit" in _config_args
|
||
PYTHON_FREE_THREADED = _config_vars.get("Py_GIL_DISABLED", 0) == 1
|
||
|
||
# get full stack traces during testing
|
||
pp.ParserElement.verbose_stacktrace = True
|
||
|
||
|
||
# simple utility for flattening nested lists
|
||
def flatten(nested_list):
|
||
if not isinstance(nested_list, list):
|
||
return [nested_list]
|
||
if not nested_list:
|
||
return nested_list
|
||
return flatten(nested_list[0]) + flatten(nested_list[1:])
|
||
|
||
|
||
class resetting:
|
||
def __init__(self, ob, attrname: str, *attrnames):
|
||
self.ob = ob
|
||
self.unset_attr = object()
|
||
self.save_attrs = [attrname, *attrnames]
|
||
self.save_values = [
|
||
getattr(ob, name, self.unset_attr) for name in self.save_attrs
|
||
]
|
||
|
||
def __enter__(self):
|
||
pass
|
||
|
||
def __exit__(self, *args):
|
||
for attr, value in zip(self.save_attrs, self.save_values):
|
||
if value is not self.unset_attr:
|
||
setattr(self.ob, attr, value)
|
||
else:
|
||
delattr(self.ob, attr)
|
||
|
||
|
||
def current_method_name(level=2):
|
||
import traceback
|
||
|
||
stack = traceback.extract_stack(limit=level)
|
||
return stack[0].name
|
||
|
||
|
||
def __():
|
||
return f"{current_method_name(3)}: "
|
||
|
||
|
||
class TestCase(unittest.TestCase):
|
||
@contextlib.contextmanager
|
||
def assertRaises(self, expected_exception_type: Any, msg: Any = None):
|
||
"""
|
||
Simple wrapper to print out the exceptions raised after assertRaises
|
||
"""
|
||
with super().assertRaises(expected_exception_type, msg=msg) as ar:
|
||
yield
|
||
|
||
if getattr(ar, "exception", None) is not None:
|
||
print(
|
||
f"Raised expected exception: {type(ar.exception).__name__}: {ar.exception}"
|
||
)
|
||
else:
|
||
print(f"Expected {expected_exception_type.__name__} exception not raised")
|
||
return ar
|
||
|
||
@contextlib.contextmanager
|
||
def assertWarns(self, expected_warning_type: Any, msg: Any = None):
|
||
"""
|
||
Simple wrapper to print out the warnings raised after assertWarns
|
||
"""
|
||
with super().assertWarns(expected_warning_type, msg=msg) as ar:
|
||
yield
|
||
|
||
if getattr(ar, "warning", None) is not None:
|
||
print(f"Raised expected warning: {type(ar.warning).__name__}: {ar.warning}")
|
||
else:
|
||
print(f"Expected {expected_warning_type.__name__} warning not raised")
|
||
return ar
|
||
|
||
@contextlib.contextmanager
|
||
def assertDoesNotWarn(self, warning_type: type = UserWarning, msg: str = None):
|
||
with warnings.catch_warnings(record=True) as w:
|
||
warnings.simplefilter("error")
|
||
try:
|
||
yield
|
||
except Exception as e:
|
||
if msg is None:
|
||
msg = f"unexpected warning {e} raised"
|
||
if isinstance(e, warning_type):
|
||
self.fail(f"{msg}: {e}")
|
||
else:
|
||
raise
|
||
finally:
|
||
warnings.simplefilter("default")
|
||
|
||
@contextlib.contextmanager
|
||
def resetting_recursion_limit(self):
|
||
"""Reset the recursion limit to its original value after
|
||
the context manager exits."""
|
||
orig_recursion_limit = sys.getrecursionlimit()
|
||
try:
|
||
yield
|
||
finally:
|
||
sys.setrecursionlimit(orig_recursion_limit)
|
||
|
||
class Test01_PyparsingTestInit(TestCase):
|
||
def runTest(self):
|
||
print(
|
||
"Beginning test of pyparsing, version",
|
||
pp.__version__,
|
||
pp.__version_time__,
|
||
)
|
||
config_options = []
|
||
if PYTHON_JIT_ENABLED:
|
||
config_options.append("JIT enabled")
|
||
if PYTHON_FREE_THREADED:
|
||
config_options.append("free_threaded")
|
||
config_options_str = f" ({','.join(config_options)})"
|
||
print(
|
||
f"Python version {sys.version}"
|
||
f"{config_options_str if config_options else ''}"
|
||
)
|
||
print(f"__version_info__ : {pp.__version_info__}")
|
||
print(f"__version_info__ repr: {repr(pp.__version_info__)}")
|
||
|
||
|
||
class Test01a_PyparsingEnvironmentTests(TestCase):
|
||
def runTest(self):
|
||
# test warnings enable detection
|
||
# fmt: off
|
||
tests = [
|
||
(([], "",), False),
|
||
((["d", ], "",), True),
|
||
((["d", "i:::pyparsing", ], "",), False),
|
||
((["d:::pyparsing", ], "",), True),
|
||
((["d:::pyparsing", "i", ], "",), False),
|
||
((["d:::blah", ], "",), False),
|
||
((["i", ], "",), False),
|
||
(([], "1",), True),
|
||
((["d", ], "1",), True),
|
||
((["d", "i:::pyparsing", ], "1",), False),
|
||
((["d:::pyparsing", ], "1",), True),
|
||
((["d:::pyparsing", "i", ], "1",), False),
|
||
((["d:::blah", ], "1",), True),
|
||
((["i", ], "1",), False),
|
||
]
|
||
# fmt: on
|
||
|
||
all_success = True
|
||
for args, expected in tests:
|
||
message = f"{args} should be {expected}"
|
||
print(message, end=" -> ")
|
||
actual = pp.core._should_enable_warnings(*args)
|
||
print("PASS" if actual == expected else "FAIL")
|
||
if actual != expected:
|
||
all_success = False
|
||
self.assertTrue(all_success, "failed warnings enable test")
|
||
|
||
|
||
class Test01b_PyparsingUnitTestUtilitiesTests(TestCase):
|
||
def runTest(self):
|
||
with ppt.reset_pyparsing_context():
|
||
pp.enable_diag(pp.Diagnostics.warn_on_parse_using_empty_Forward)
|
||
|
||
# test assertDoesNotWarn raises an AssertionError
|
||
with self.assertRaises(AssertionError):
|
||
with self.assertDoesNotWarn(
|
||
msg="warned when parsing with an empty Forward expression warning was suppressed",
|
||
):
|
||
base = pp.Forward()
|
||
try:
|
||
print(base.parse_string("x"))
|
||
except ParseException as pe:
|
||
pass
|
||
|
||
|
||
class Test02_WithoutPackrat(ppt.TestParseResultsAsserts, TestCase):
|
||
suite_context = None
|
||
save_suite_context = None
|
||
|
||
def setUp(self):
|
||
self.suite_context.restore()
|
||
|
||
def test000_assert_packrat_status(self):
|
||
print("Packrat enabled:", ParserElement._packratEnabled)
|
||
self.assertFalse(ParserElement._packratEnabled, "packrat enabled")
|
||
|
||
def testScanStringWithOverlap(self):
|
||
parser = pp.Word(pp.alphas, exact=3)
|
||
without_overlaps = sum(
|
||
t for t, s, e in parser.scan_string("ABCDEFGHI")
|
||
).as_list()
|
||
self.assertEqual(
|
||
["ABC", "DEF", "GHI"],
|
||
without_overlaps,
|
||
msg="scan_string without overlaps failed",
|
||
)
|
||
with_overlaps = sum(
|
||
t for t, s, e in parser.scan_string("ABCDEFGHI", overlap=True)
|
||
).as_list()
|
||
self.assertEqual(
|
||
["ABC", "BCD", "CDE", "DEF", "EFG", "FGH", "GHI"],
|
||
with_overlaps,
|
||
msg="scan_string with overlaps failed",
|
||
)
|
||
|
||
def testCombineWithResultsNames(self):
|
||
# test case reproducing Issue #350
|
||
from pyparsing import White, alphas, Word
|
||
|
||
parser = White(" \t").set_results_name("indent") + Word(
|
||
alphas
|
||
).set_results_name("word")
|
||
result = parser.parse_string(" test")
|
||
print(result.dump())
|
||
self.assertParseResultsEquals(
|
||
result, [" ", "test"], {"indent": " ", "word": "test"}
|
||
)
|
||
|
||
parser = White(" \t") + Word(alphas).set_results_name("word")
|
||
result = parser.parse_string(" test")
|
||
print(result.dump())
|
||
self.assertParseResultsEquals(result, [" ", "test"], {"word": "test"})
|
||
|
||
def testTransformString(self):
|
||
make_int_with_commas = ppc.integer().add_parse_action(lambda t: f"{t[0]:,}")
|
||
lower_case_words = pp.Word(pp.alphas.lower(), as_keyword=True) + pp.Optional(
|
||
pp.White()
|
||
)
|
||
nested_list = pp.nested_expr().add_parse_action(pp.ParseResults.as_list)
|
||
transformer = make_int_with_commas | nested_list | lower_case_words.suppress()
|
||
|
||
in_string = (
|
||
"I wish to buy 12345 shares of Acme Industries (as a gift to my (ex)wife)"
|
||
)
|
||
print(in_string)
|
||
out_string = transformer.transform_string(in_string)
|
||
print(out_string)
|
||
self.assertEqual(
|
||
"I 12,345 Acme Industries asagifttomyexwife",
|
||
out_string,
|
||
msg="failure in transform_string",
|
||
)
|
||
|
||
def testTransformStringWithLeadingWhitespace(self):
|
||
sample = "\n\ncheck"
|
||
sample = " check"
|
||
keywords = pp.one_of("aaa bbb", as_keyword=True)
|
||
ident = ~keywords + pp.Word(pp.alphas)
|
||
ident = pp.Combine(~keywords + pp.Word(pp.alphas))
|
||
# ident.add_parse_action(lambda t: t[0].upper())
|
||
ident.add_parse_action(ppc.upcase_tokens)
|
||
transformed = ident.transform_string(sample)
|
||
|
||
print(ppt.with_line_numbers(sample))
|
||
print(ppt.with_line_numbers(transformed))
|
||
self.assertEqual(sample.replace("check", "CHECK"), transformed)
|
||
|
||
def testTransformStringWithLeadingNotAny(self):
|
||
sample = "print a100"
|
||
keywords = set("print read".split())
|
||
ident = pp.Word(pp.alphas, pp.alphanums).add_condition(
|
||
lambda t: t[0] not in keywords
|
||
)
|
||
print(ident.search_string(sample))
|
||
|
||
def testTransformStringWithExpectedLeadingWhitespace(self):
|
||
sample1 = "\n\ncheck aaa"
|
||
sample2 = " check aaa"
|
||
keywords = pp.one_of("aaa bbb", as_keyword=True)
|
||
# This construct only works with parse_string, not with scan_string or its siblings
|
||
# ident = ~keywords + pp.Word(pp.alphas)
|
||
ident = pp.Word(pp.alphas)
|
||
ident.add_parse_action(ppc.upcase_tokens)
|
||
|
||
for sample in sample1, sample2:
|
||
transformed = (keywords | ident).transform_string(sample)
|
||
print(ppt.with_line_numbers(sample))
|
||
print(ppt.with_line_numbers(transformed))
|
||
self.assertEqual(sample.replace("check", "CHECK"), transformed)
|
||
print()
|
||
|
||
def testTransformStringWithLeadingWhitespaceFromTranslateProject(self):
|
||
from pyparsing import Keyword, Word, alphas, alphanums, Combine
|
||
|
||
block_start = (Keyword("{") | Keyword("BEGIN")).set_name("block_start")
|
||
block_end = (Keyword("}") | Keyword("END")).set_name("block_end")
|
||
reserved_words = block_start | block_end
|
||
|
||
# this is the first critical part of this test, an And with a leading NotAny
|
||
# This construct only works with parse_string, not with scan_string or its siblings
|
||
# name_id = ~reserved_words + Word(alphas, alphanums + "_").set_name("name_id")
|
||
name_id = Word(alphas, alphanums + "_").set_name("name_id")
|
||
|
||
dialog = name_id("block_id") + (Keyword("DIALOGEX") | Keyword("DIALOG"))(
|
||
"block_type"
|
||
)
|
||
string_table = Keyword("STRINGTABLE")("block_type")
|
||
|
||
test_string = (
|
||
"""\r\nSTRINGTABLE\r\nBEGIN\r\n// Comment\r\nIDS_1 "Copied"\r\nEND\r\n"""
|
||
)
|
||
print("Original:")
|
||
print(repr(test_string))
|
||
print("Should match:")
|
||
# this is the second critical part of this test, an Or or MatchFirst including dialog
|
||
for parser in (dialog ^ string_table, dialog | string_table):
|
||
result = (reserved_words | parser).transform_string(test_string)
|
||
print(repr(result))
|
||
self.assertEqual(
|
||
test_string,
|
||
result,
|
||
"Failed whitespace skipping with NotAny and MatchFirst/Or",
|
||
)
|
||
|
||
def testCuneiformTransformString(self):
|
||
|
||
class Cuneiform(pp.unicode_set):
|
||
"""Unicode set for Cuneiform Character Range"""
|
||
|
||
_ranges: list[tuple[int, ...]] = [
|
||
(0x10380, 0x103D5),
|
||
(0x12000, 0x123FF),
|
||
(0x12400, 0x1247F),
|
||
]
|
||
|
||
# define a MINIMAL Python parser
|
||
LPAR, RPAR, COLON, EQ = map(pp.Suppress, "():=")
|
||
def_ = pp.Keyword("𒁴𒈫", ident_chars=Cuneiform.identbodychars).set_name("def")
|
||
any_keyword = def_
|
||
ident = (~any_keyword) + pp.Word(
|
||
Cuneiform.identchars, Cuneiform.identbodychars, as_keyword=True
|
||
)
|
||
str_expr = pp.infix_notation(
|
||
pp.QuotedString('"') | pp.common.integer,
|
||
[
|
||
("*", 2, pp.OpAssoc.LEFT),
|
||
("+", 2, pp.OpAssoc.LEFT),
|
||
],
|
||
)
|
||
|
||
rvalue = pp.Forward()
|
||
fn_call = (ident + pp.Group(LPAR + pp.Optional(rvalue) + RPAR)).set_name(
|
||
"fn_call"
|
||
)
|
||
|
||
rvalue <<= fn_call | ident | str_expr | pp.common.number
|
||
assignment_stmt = ident + EQ + rvalue
|
||
|
||
stmt = pp.Group(fn_call | assignment_stmt).set_name("stmt")
|
||
|
||
fn_def = pp.Group(
|
||
def_ + ident + pp.Group(LPAR + pp.Optional(rvalue) + RPAR) + COLON
|
||
).set_name("fn_def")
|
||
fn_body = pp.IndentedBlock(stmt).set_name("fn_body")
|
||
fn_expr = pp.Group(fn_def + pp.Group(fn_body))
|
||
|
||
script = fn_expr[...] + stmt[...]
|
||
|
||
# parse some Python written in Cuneiform
|
||
cuneiform_hello_world = dedent(
|
||
r"""
|
||
𒁴𒈫 𒀄𒂖𒆷𒁎():
|
||
𒀁 = "𒀄𒂖𒆷𒁎, 𒍟𒁎𒉿𒆷𒀳!\n" * 3
|
||
𒄑𒉿𒅔𒋫(𒀁)
|
||
|
||
𒀄𒂖𒆷𒁎()
|
||
"""
|
||
)
|
||
|
||
# use transform_string to convert keywords and builtins to runnable Python
|
||
names_map = {
|
||
"𒄑𒉿𒅔𒋫": "print",
|
||
}
|
||
ident.add_parse_action(lambda t: names_map.get(t[0], t[0]))
|
||
def_.add_parse_action(lambda: "def")
|
||
|
||
print("\nconvert Cuneiform Python to executable Python")
|
||
transformed = (
|
||
# always put ident last
|
||
(def_ | ident)
|
||
.ignore(pp.quoted_string)
|
||
.transform_string(cuneiform_hello_world)
|
||
)
|
||
|
||
expected = dedent(
|
||
r"""
|
||
def 𒀄𒂖𒆷𒁎():
|
||
𒀁 = "𒀄𒂖𒆷𒁎, 𒍟𒁎𒉿𒆷𒀳!\n" * 3
|
||
print(𒀁)
|
||
|
||
𒀄𒂖𒆷𒁎()
|
||
"""
|
||
)
|
||
|
||
print(
|
||
"=================\n"
|
||
+ cuneiform_hello_world # .strip()
|
||
+ "\n=================\n"
|
||
+ transformed
|
||
+ "\n=================\n"
|
||
)
|
||
|
||
self.assertEqual(expected, transformed)
|
||
|
||
def testUpdateDefaultWhitespace(self):
|
||
prev_default_whitespace_chars = pp.ParserElement.DEFAULT_WHITE_CHARS
|
||
try:
|
||
pp.dbl_quoted_string.copyDefaultWhiteChars = False
|
||
pp.ParserElement.set_default_whitespace_chars(" \t")
|
||
self.assertEqual(
|
||
set(" \t"),
|
||
set(pp.sgl_quoted_string.whiteChars),
|
||
"set_default_whitespace_chars did not update sgl_quoted_string",
|
||
)
|
||
self.assertEqual(
|
||
set(prev_default_whitespace_chars),
|
||
set(pp.dbl_quoted_string.whiteChars),
|
||
"set_default_whitespace_chars updated dbl_quoted_string but should not",
|
||
)
|
||
finally:
|
||
pp.dbl_quoted_string.copyDefaultWhiteChars = True
|
||
pp.ParserElement.set_default_whitespace_chars(prev_default_whitespace_chars)
|
||
|
||
self.assertEqual(
|
||
set(prev_default_whitespace_chars),
|
||
set(pp.dbl_quoted_string.whiteChars),
|
||
"set_default_whitespace_chars updated dbl_quoted_string",
|
||
)
|
||
|
||
with ppt.reset_pyparsing_context():
|
||
pp.ParserElement.set_default_whitespace_chars(" \t")
|
||
self.assertNotEqual(
|
||
set(prev_default_whitespace_chars),
|
||
set(pp.dbl_quoted_string.whiteChars),
|
||
"set_default_whitespace_chars updated dbl_quoted_string but should not",
|
||
)
|
||
|
||
EOL = pp.LineEnd().suppress().set_name("EOL")
|
||
|
||
# Identifiers is a string + optional $
|
||
identifier = pp.Combine(pp.Word(pp.alphas) + pp.Optional("$"))
|
||
|
||
# Literals (number or double quoted string)
|
||
literal = ppc.number | pp.dbl_quoted_string
|
||
expression = literal | identifier
|
||
# expression.set_name("expression").set_debug()
|
||
# ppc.number.set_debug()
|
||
# ppc.integer.set_debug()
|
||
|
||
line_number = ppc.integer
|
||
|
||
# Keywords
|
||
PRINT = pp.CaselessKeyword("print")
|
||
print_stmt = PRINT - pp.ZeroOrMore(expression | ";")
|
||
statement = print_stmt
|
||
code_line = pp.Group(line_number + statement + EOL)
|
||
program = pp.ZeroOrMore(code_line)
|
||
|
||
test = """\
|
||
10 print 123;
|
||
20 print 234; 567;
|
||
30 print 890
|
||
"""
|
||
|
||
parsed_program = program.parse_string(test, parse_all=True)
|
||
print(parsed_program.dump())
|
||
self.assertEqual(
|
||
3,
|
||
len(parsed_program),
|
||
"failed to apply new whitespace chars to existing builtins",
|
||
)
|
||
|
||
def testUpdateDefaultWhitespace2(self):
|
||
with ppt.reset_pyparsing_context():
|
||
expr_tests = [
|
||
(pp.dbl_quoted_string, '"abc"'),
|
||
(pp.sgl_quoted_string, "'def'"),
|
||
(ppc.integer, "123"),
|
||
(ppc.number, "4.56"),
|
||
(ppc.identifier, "a_bc"),
|
||
]
|
||
NL = pp.LineEnd()
|
||
|
||
for expr, test_str in expr_tests:
|
||
parser = pp.Group(expr[1, ...] + pp.Optional(NL))[1, ...]
|
||
test_string = "\n".join([test_str] * 3)
|
||
result = parser.parse_string(test_string, parse_all=True)
|
||
print(result.dump())
|
||
self.assertEqual(1, len(result), f"failed {test_string!r}")
|
||
|
||
pp.ParserElement.set_default_whitespace_chars(" \t")
|
||
|
||
for expr, test_str in expr_tests:
|
||
parser = pp.Group(expr[1, ...] + pp.Optional(NL))[1, ...]
|
||
test_string = "\n".join([test_str] * 3)
|
||
result = parser.parse_string(test_string, parse_all=True)
|
||
print(result.dump())
|
||
self.assertEqual(3, len(result), f"failed {test_string!r}")
|
||
|
||
pp.ParserElement.set_default_whitespace_chars(" \n\t")
|
||
|
||
for expr, test_str in expr_tests:
|
||
parser = pp.Group(expr[1, ...] + pp.Optional(NL))[1, ...]
|
||
test_string = "\n".join([test_str] * 3)
|
||
result = parser.parse_string(test_string, parse_all=True)
|
||
print(result.dump())
|
||
self.assertEqual(1, len(result), f"failed {test_string!r}")
|
||
|
||
def testParseFourFn(self):
|
||
import examples.fourFn as fourFn
|
||
import math
|
||
|
||
def test(s, ans):
|
||
fourFn.exprStack[:] = []
|
||
results = fourFn.BNF().parse_string(s, parse_all=True)
|
||
try:
|
||
resultValue = fourFn.evaluate_stack(fourFn.exprStack)
|
||
except Exception:
|
||
self.assertIsNone(ans, f"exception raised for expression {s!r}")
|
||
else:
|
||
self.assertEqual(
|
||
ans,
|
||
resultValue,
|
||
f"failed to evaluate {s}, got {resultValue:f}",
|
||
)
|
||
print(s, "->", resultValue)
|
||
|
||
test("9", 9)
|
||
test("-9", -9)
|
||
test("--9", 9)
|
||
test("-E", -math.e)
|
||
test("9 + 3 + 5", 9 + 3 + 5)
|
||
test("9 + 3 / 11", 9 + 3.0 / 11)
|
||
test("(9 + 3)", (9 + 3))
|
||
test("(9+3) / 11", (9 + 3.0) / 11)
|
||
test("9 - 12 - 6", 9 - 12 - 6)
|
||
test("9 - (12 - 6)", 9 - (12 - 6))
|
||
test("2*3.14159", 2 * 3.14159)
|
||
test("3.1415926535*3.1415926535 / 10", 3.1415926535 * 3.1415926535 / 10)
|
||
test("PI * PI / 10", math.pi * math.pi / 10)
|
||
test("PI*PI/10", math.pi * math.pi / 10)
|
||
test("PI^2", math.pi**2)
|
||
test("round(PI^2)", round(math.pi**2))
|
||
test("6.02E23 * 8.048", 6.02e23 * 8.048)
|
||
test("e / 3", math.e / 3)
|
||
test("sin(PI/2)", math.sin(math.pi / 2))
|
||
test("10+sin(PI/4)^2", 10 + math.sin(math.pi / 4) ** 2)
|
||
test("trunc(E)", int(math.e))
|
||
test("trunc(-E)", int(-math.e))
|
||
test("round(E)", round(math.e))
|
||
test("round(-E)", round(-math.e))
|
||
test("E^PI", math.e**math.pi)
|
||
test("exp(0)", 1)
|
||
test("exp(1)", math.e)
|
||
test("2^3^2", 2**3**2)
|
||
test("(2^3)^2", (2**3) ** 2)
|
||
test("2^3+2", 2**3 + 2)
|
||
test("2^3+5", 2**3 + 5)
|
||
test("2^9", 2**9)
|
||
test("sgn(-2)", -1)
|
||
test("sgn(0)", 0)
|
||
test("sgn(0.1)", 1)
|
||
test("foo(0.1)", None)
|
||
test("round(E, 3)", round(math.e, 3))
|
||
test("round(PI^2, 3)", round(math.pi**2, 3))
|
||
test("sgn(cos(PI/4))", 1)
|
||
test("sgn(cos(PI/2))", 0)
|
||
test("sgn(cos(PI*3/4))", -1)
|
||
test("+(sgn(cos(PI/4)))", 1)
|
||
test("-(sgn(cos(PI/4)))", -1)
|
||
|
||
def testParseSQL(self):
|
||
# SQL parser uses packrat parsing, not compatible with LR
|
||
if ParserElement._left_recursion_enabled:
|
||
return
|
||
|
||
import examples.simpleSQL as simpleSQL
|
||
|
||
def test(s, num_expected_toks, expected_errloc=-1):
|
||
try:
|
||
sqlToks = flatten(
|
||
simpleSQL.simpleSQL.parse_string(s, parse_all=True).as_list()
|
||
)
|
||
print(s, sqlToks, len(sqlToks))
|
||
self.assertEqual(
|
||
num_expected_toks,
|
||
len(sqlToks),
|
||
f"invalid parsed tokens, expected {num_expected_toks}, found {len(sqlToks)} ({sqlToks})",
|
||
)
|
||
except ParseException as e:
|
||
if expected_errloc >= 0:
|
||
self.assertEqual(
|
||
expected_errloc,
|
||
e.loc,
|
||
f"expected error at {expected_errloc}, found at {e.loc}",
|
||
)
|
||
|
||
test("SELECT * from XYZZY, ABC", 6)
|
||
test("select * from SYS.XYZZY", 5)
|
||
test("Select A from Sys.dual", 5)
|
||
test("Select A,B,C from Sys.dual", 7)
|
||
test("Select A, B, C from Sys.dual", 7)
|
||
test("Select A, B, C from Sys.dual, Table2 ", 8)
|
||
test("Xelect A, B, C from Sys.dual", 0, 0)
|
||
test("Select A, B, C frox Sys.dual", 0, 15)
|
||
test("Select", 0, 6)
|
||
test("Select &&& frox Sys.dual", 0, 7)
|
||
test("Select A from Sys.dual where a in ('RED','GREEN','BLUE')", 12)
|
||
test(
|
||
"Select A from Sys.dual where a in ('RED','GREEN','BLUE') and b in (10,20,30)",
|
||
20,
|
||
)
|
||
test(
|
||
"Select A,b from table1,table2 where table1.id eq table2.id -- test out comparison operators",
|
||
10,
|
||
)
|
||
|
||
def testParseConfigFile(self):
|
||
from examples import configParse
|
||
|
||
def test(fnam, num_expected_toks, resCheckList):
|
||
print("Parsing", fnam, "...", end=" ")
|
||
with open(fnam) as infile:
|
||
iniFileLines = "\n".join(infile.read().splitlines())
|
||
iniData = configParse.inifile_BNF().parse_string(
|
||
iniFileLines, parse_all=True
|
||
)
|
||
print(len(flatten(iniData.as_list())))
|
||
print(list(iniData.keys()))
|
||
self.assertEqual(
|
||
num_expected_toks,
|
||
len(flatten(iniData.as_list())),
|
||
f"file {fnam} not parsed correctly",
|
||
)
|
||
for chkkey, chkexpect in resCheckList:
|
||
var = iniData
|
||
for attr in chkkey.split("."):
|
||
var = getattr(var, attr)
|
||
print(chkkey, var, chkexpect)
|
||
self.assertEqual(
|
||
chkexpect,
|
||
var,
|
||
f"ParseConfigFileTest: failed to parse ini {chkkey!r} as expected {chkexpect!r}, found {var}",
|
||
)
|
||
print("OK")
|
||
|
||
test(
|
||
"tests/karthik.ini",
|
||
23,
|
||
[("users.K", "8"), ("users.mod_scheme", "'QPSK'"), ("users.Na", "K+2")],
|
||
)
|
||
test(
|
||
"examples/Setup.ini",
|
||
125,
|
||
[
|
||
("Startup.audioinf", "M3i"),
|
||
("Languages.key1", "0x0003"),
|
||
("test.foo", "bar"),
|
||
],
|
||
)
|
||
|
||
def testParseJSONData(self):
|
||
expected = [
|
||
{
|
||
"glossary": {
|
||
"GlossDiv": {
|
||
"GlossList": [
|
||
{
|
||
"Abbrev": "ISO 8879:1986",
|
||
"Acronym": "SGML",
|
||
"AvogadroNumber": 6.02e23,
|
||
"EmptyDict": {},
|
||
"EmptyList": [],
|
||
"EvenPrimesGreaterThan2": [],
|
||
"FermatTheoremInMargin": False,
|
||
"GlossDef": "A meta-markup language, "
|
||
"used to create markup "
|
||
"languages such as "
|
||
"DocBook.",
|
||
"GlossSeeAlso": ["GML", "XML", "markup"],
|
||
"GlossTerm": "Standard Generalized " "Markup Language",
|
||
"ID": "SGML",
|
||
"LargestPrimeLessThan100": 97,
|
||
"MapRequiringFiveColors": None,
|
||
"PrimesLessThan10": [2, 3, 5, 7],
|
||
"SortAs": "SGML",
|
||
}
|
||
],
|
||
"title": "S",
|
||
},
|
||
"title": "example glossary",
|
||
}
|
||
},
|
||
{
|
||
"menu": {
|
||
"id": "file",
|
||
"popup": {
|
||
"menuitem": [
|
||
{"onclick": "CreateNewDoc()", "value": "New"},
|
||
{"onclick": "OpenDoc()", "value": "Open"},
|
||
{"onclick": "CloseDoc()", "value": "Close"},
|
||
]
|
||
},
|
||
"value": "File:",
|
||
}
|
||
},
|
||
{
|
||
"widget": {
|
||
"debug": "on",
|
||
"image": {
|
||
"alignment": "center",
|
||
"hOffset": 250,
|
||
"name": "sun1",
|
||
"src": "Images/Sun.png",
|
||
"vOffset": 250,
|
||
},
|
||
"text": {
|
||
"alignment": "center",
|
||
"data": "Click Here",
|
||
"hOffset": 250,
|
||
"name": "text1",
|
||
"onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;",
|
||
"size": 36,
|
||
"style": "bold",
|
||
"vOffset": 100,
|
||
},
|
||
"window": {
|
||
"height": 500,
|
||
"name": "main_window",
|
||
"title": "Sample Konfabulator Widget",
|
||
"width": 500,
|
||
},
|
||
}
|
||
},
|
||
{
|
||
"web-app": {
|
||
"servlet": [
|
||
{
|
||
"init-param": {
|
||
"cachePackageTagsRefresh": 60,
|
||
"cachePackageTagsStore": 200,
|
||
"cachePackageTagsTrack": 200,
|
||
"cachePagesDirtyRead": 10,
|
||
"cachePagesRefresh": 10,
|
||
"cachePagesStore": 100,
|
||
"cachePagesTrack": 200,
|
||
"cacheTemplatesRefresh": 15,
|
||
"cacheTemplatesStore": 50,
|
||
"cacheTemplatesTrack": 100,
|
||
"configGlossary:adminEmail": "ksm@pobox.com",
|
||
"configGlossary:installationAt": "Philadelphia, " "PA",
|
||
"configGlossary:poweredBy": "Cofax",
|
||
"configGlossary:poweredByIcon": "/images/cofax.gif",
|
||
"configGlossary:staticPath": "/content/static",
|
||
"dataStoreClass": "org.cofax.SqlDataStore",
|
||
"dataStoreConnUsageLimit": 100,
|
||
"dataStoreDriver": "com.microsoft.jdbc.sqlserver.SQLServerDriver",
|
||
"dataStoreInitConns": 10,
|
||
"dataStoreLogFile": "/usr/local/tomcat/logs/datastore.log",
|
||
"dataStoreLogLevel": "debug",
|
||
"dataStoreMaxConns": 100,
|
||
"dataStoreName": "cofax",
|
||
"dataStorePassword": "dataStoreTestQuery",
|
||
"dataStoreTestQuery": "SET NOCOUNT "
|
||
"ON;select "
|
||
"test='test';",
|
||
"dataStoreUrl": "jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon",
|
||
"dataStoreUser": "sa",
|
||
"defaultFileTemplate": "articleTemplate.htm",
|
||
"defaultListTemplate": "listTemplate.htm",
|
||
"jspFileTemplate": "articleTemplate.jsp",
|
||
"jspListTemplate": "listTemplate.jsp",
|
||
"maxUrlLength": 500,
|
||
"redirectionClass": "org.cofax.SqlRedirection",
|
||
"searchEngineFileTemplate": "forSearchEngines.htm",
|
||
"searchEngineListTemplate": "forSearchEnginesList.htm",
|
||
"searchEngineRobotsDb": "WEB-INF/robots.db",
|
||
"templateLoaderClass": "org.cofax.FilesTemplateLoader",
|
||
"templateOverridePath": "",
|
||
"templatePath": "templates",
|
||
"templateProcessorClass": "org.cofax.WysiwygTemplate",
|
||
"useDataStore": True,
|
||
"useJSP": False,
|
||
},
|
||
"servlet-class": "org.cofax.cds.CDSServlet",
|
||
"servlet-name": "cofaxCDS",
|
||
},
|
||
{
|
||
"init-param": {
|
||
"mailHost": "mail1",
|
||
"mailHostOverride": "mail2",
|
||
},
|
||
"servlet-class": "org.cofax.cds.EmailServlet",
|
||
"servlet-name": "cofaxEmail",
|
||
},
|
||
{
|
||
"servlet-class": "org.cofax.cds.AdminServlet",
|
||
"servlet-name": "cofaxAdmin",
|
||
},
|
||
{
|
||
"servlet-class": "org.cofax.cds.FileServlet",
|
||
"servlet-name": "fileServlet",
|
||
},
|
||
{
|
||
"init-param": {
|
||
"adminGroupID": 4,
|
||
"betaServer": True,
|
||
"dataLog": 1,
|
||
"dataLogLocation": "/usr/local/tomcat/logs/dataLog.log",
|
||
"dataLogMaxSize": "",
|
||
"fileTransferFolder": "/usr/local/tomcat/webapps/content/fileTransferFolder",
|
||
"log": 1,
|
||
"logLocation": "/usr/local/tomcat/logs/CofaxTools.log",
|
||
"logMaxSize": "",
|
||
"lookInContext": 1,
|
||
"removePageCache": "/content/admin/remove?cache=pages&id=",
|
||
"removeTemplateCache": "/content/admin/remove?cache=templates&id=",
|
||
"templatePath": "toolstemplates/",
|
||
},
|
||
"servlet-class": "org.cofax.cms.CofaxToolsServlet",
|
||
"servlet-name": "cofaxTools",
|
||
},
|
||
],
|
||
"servlet-mapping": {
|
||
"cofaxAdmin": "/admin/*",
|
||
"cofaxCDS": "/",
|
||
"cofaxEmail": "/cofaxutil/aemail/*",
|
||
"cofaxTools": "/tools/*",
|
||
"fileServlet": "/static/*",
|
||
},
|
||
"taglib": {
|
||
"taglib-location": "/WEB-INF/tlds/cofax.tld",
|
||
"taglib-uri": "cofax.tld",
|
||
},
|
||
}
|
||
},
|
||
{
|
||
"menu": {
|
||
"header": "SVG Viewer",
|
||
"items": [
|
||
{"id": "Open"},
|
||
{"id": "OpenNew", "label": "Open New"},
|
||
None,
|
||
{"id": "ZoomIn", "label": "Zoom In"},
|
||
{"id": "ZoomOut", "label": "Zoom Out"},
|
||
{"id": "OriginalView", "label": "Original View"},
|
||
None,
|
||
{"id": "Quality"},
|
||
{"id": "Pause"},
|
||
{"id": "Mute"},
|
||
None,
|
||
{"id": "Find", "label": "Find..."},
|
||
{"id": "FindAgain", "label": "Find Again"},
|
||
{"id": "Copy"},
|
||
{"id": "CopyAgain", "label": "Copy Again"},
|
||
{"id": "CopySVG", "label": "Copy SVG"},
|
||
{"id": "ViewSVG", "label": "View SVG"},
|
||
{"id": "ViewSource", "label": "View Source"},
|
||
{"id": "SaveAs", "label": "Save As"},
|
||
None,
|
||
{"id": "Help"},
|
||
{"id": "About", "label": "About Adobe CVG Viewer..."},
|
||
],
|
||
}
|
||
},
|
||
]
|
||
|
||
for t, exp_result in zip((test1, test2, test3, test4, test5), expected):
|
||
result = jsonObject.parse_string(t, parse_all=True)
|
||
self.assertEqual(exp_result, result[0])
|
||
|
||
def testParseCommaSeparatedValues(self):
|
||
testData = [
|
||
"a,b,c,100.2,,3",
|
||
"d, e, j k , m ",
|
||
"'Hello, World', f, g , , 5.1,x",
|
||
"John Doe, 123 Main St., Cleveland, Ohio",
|
||
"Jane Doe, 456 St. James St., Los Angeles , California ",
|
||
"",
|
||
]
|
||
testVals = [
|
||
[(3, "100.2"), (4, ""), (5, "3")],
|
||
[(2, "j k"), (3, "m")],
|
||
[(0, "'Hello, World'"), (2, "g"), (3, "")],
|
||
[(0, "John Doe"), (1, "123 Main St."), (2, "Cleveland"), (3, "Ohio")],
|
||
[
|
||
(0, "Jane Doe"),
|
||
(1, "456 St. James St."),
|
||
(2, "Los Angeles"),
|
||
(3, "California"),
|
||
],
|
||
]
|
||
for line, tests in zip(testData, testVals):
|
||
print(f"Parsing: {line!r} ->", end=" ")
|
||
results = ppc.comma_separated_list.parse_string(line, parse_all=True)
|
||
print(results)
|
||
for t in tests:
|
||
if not (len(results) > t[0] and results[t[0]] == t[1]):
|
||
print("$$$", results.dump())
|
||
print("$$$", results[0])
|
||
self.assertTrue(
|
||
len(results) > t[0] and results[t[0]] == t[1],
|
||
f"failed on {line}, item {t[0]:d} s/b '{t[1]}', got '{results.as_list()}'",
|
||
)
|
||
|
||
def testParseEBNF(self):
|
||
from examples import ebnf
|
||
|
||
print("Constructing EBNF parser with pyparsing...")
|
||
|
||
grammar = """
|
||
syntax = (syntax_rule), {(syntax_rule)};
|
||
syntax_rule = meta_identifier, '=', definitions_list, ';';
|
||
definitions_list = single_definition, {'|', single_definition};
|
||
single_definition = syntactic_term, {',', syntactic_term};
|
||
syntactic_term = syntactic_factor,['-', syntactic_factor];
|
||
syntactic_factor = [integer, '*'], syntactic_primary;
|
||
syntactic_primary = optional_sequence | repeated_sequence |
|
||
grouped_sequence | meta_identifier | terminal_string;
|
||
optional_sequence = '[', definitions_list, ']';
|
||
repeated_sequence = '{', definitions_list, '}';
|
||
grouped_sequence = '(', definitions_list, ')';
|
||
(*
|
||
terminal_string = "'", character - "'", {character - "'"}, "'" |
|
||
'"', character - '"', {character - '"'}, '"';
|
||
meta_identifier = letter, {letter | digit};
|
||
integer = digit, {digit};
|
||
*)
|
||
"""
|
||
|
||
table = {}
|
||
table["terminal_string"] = pp.quoted_string
|
||
table["meta_identifier"] = pp.Word(pp.alphas + "_", pp.alphas + "_" + pp.nums)
|
||
table["integer"] = pp.Word(pp.nums)
|
||
|
||
print("Parsing EBNF grammar with EBNF parser...")
|
||
parsers = ebnf.parse(grammar, table)
|
||
ebnf_parser = parsers["syntax"]
|
||
ebnf_comment = pp.Literal("(*") + ... + "*)"
|
||
ebnf_parser.ignore(ebnf_comment)
|
||
print("-", "\n- ".join(parsers.keys()))
|
||
self.assertEqual(
|
||
13, len(list(parsers.keys())), "failed to construct syntax grammar"
|
||
)
|
||
|
||
print("Parsing EBNF grammar with generated EBNF parser...")
|
||
parsed_chars = ebnf_parser.parse_string(grammar, parse_all=True)
|
||
parsed_char_len = len(parsed_chars)
|
||
|
||
print("],\n".join(str(parsed_chars.as_list()).split("],")))
|
||
self.assertEqual(
|
||
98,
|
||
len(flatten(parsed_chars.as_list())),
|
||
"failed to tokenize grammar correctly",
|
||
)
|
||
|
||
def testParseEBNFmissingDefinitions(self):
|
||
"""
|
||
Test detection of missing definitions in EBNF
|
||
"""
|
||
from examples import ebnf
|
||
|
||
grammar = """
|
||
(*
|
||
EBNF for number_words.py
|
||
*)
|
||
number = [thousands, [and]], [hundreds, [and]], [one_to_99];
|
||
"""
|
||
|
||
with self.assertRaisesRegex(
|
||
AssertionError,
|
||
r"Missing definitions for \['thousands', 'and', 'hundreds', 'one_to_99']",
|
||
):
|
||
ebnf.parse(grammar)
|
||
|
||
def testParseIDL(self):
|
||
from examples import idlParse
|
||
|
||
def test(strng, numToks, expectedErrloc=0):
|
||
print(strng)
|
||
try:
|
||
bnf = idlParse.CORBA_IDL_BNF()
|
||
tokens = bnf.parse_string(strng, parse_all=True)
|
||
print("tokens = ")
|
||
tokens.pprint()
|
||
tokens = flatten(tokens.as_list())
|
||
print(len(tokens))
|
||
self.assertEqual(
|
||
numToks,
|
||
len(tokens),
|
||
f"error matching IDL string, {strng} -> {tokens}",
|
||
)
|
||
except ParseException as err:
|
||
print(err.line)
|
||
print(f"{' ' * (err.column - 1)}^")
|
||
print(err)
|
||
self.assertEqual(
|
||
0,
|
||
numToks,
|
||
f"unexpected ParseException while parsing {strng}, {err}",
|
||
)
|
||
self.assertEqual(
|
||
expectedErrloc,
|
||
err.loc,
|
||
f"expected ParseException at {expectedErrloc}, found exception at {err.loc}",
|
||
)
|
||
|
||
test(
|
||
"""
|
||
/*
|
||
* a block comment *
|
||
*/
|
||
typedef string[10] tenStrings;
|
||
typedef sequence<string> stringSeq;
|
||
typedef sequence< sequence<string> > stringSeqSeq;
|
||
|
||
interface QoSAdmin {
|
||
stringSeq method1(in string arg1, inout long arg2);
|
||
stringSeqSeq method2(in string arg1, inout long arg2, inout long arg3);
|
||
string method3();
|
||
};
|
||
""",
|
||
59,
|
||
)
|
||
test(
|
||
"""
|
||
/*
|
||
* a block comment *
|
||
*/
|
||
typedef string[10] tenStrings;
|
||
typedef
|
||
/** ** *** **** *
|
||
* a block comment *
|
||
*/
|
||
sequence<string> /*comment inside an And */ stringSeq;
|
||
/* */ /**/ /***/ /****/
|
||
typedef sequence< sequence<string> > stringSeqSeq;
|
||
|
||
interface QoSAdmin {
|
||
stringSeq method1(in string arg1, inout long arg2);
|
||
stringSeqSeq method2(in string arg1, inout long arg2, inout long arg3);
|
||
string method3();
|
||
};
|
||
""",
|
||
59,
|
||
)
|
||
test(
|
||
r"""
|
||
const string test="Test String\n";
|
||
const long a = 0;
|
||
const long b = -100;
|
||
const float c = 3.14159;
|
||
const long d = 0x007f7f7f;
|
||
exception TestException
|
||
{
|
||
string msg;
|
||
sequence<string> dataStrings;
|
||
};
|
||
|
||
interface TestInterface
|
||
{
|
||
void method1(in string arg1, inout long arg2);
|
||
};
|
||
""",
|
||
60,
|
||
)
|
||
test(
|
||
"""
|
||
module Test1
|
||
{
|
||
exception TestException
|
||
{
|
||
string msg;
|
||
];
|
||
|
||
interface TestInterface
|
||
{
|
||
void method1(in string arg1, inout long arg2)
|
||
raises (TestException);
|
||
};
|
||
};
|
||
""",
|
||
0,
|
||
56,
|
||
)
|
||
test(
|
||
"""
|
||
module Test1
|
||
{
|
||
exception TestException
|
||
{
|
||
string msg;
|
||
};
|
||
|
||
};
|
||
""",
|
||
13,
|
||
)
|
||
|
||
def testParseVerilog(self):
|
||
pass
|
||
|
||
def testScanString(self):
|
||
testdata = """
|
||
<table border="0" cellpadding="3" cellspacing="3" frame="" width="90%">
|
||
<tr align="left" valign="top">
|
||
<td><b>Name</b></td>
|
||
<td><b>IP Address</b></td>
|
||
<td><b>Location</b></td>
|
||
</tr>
|
||
<tr align="left" valign="top" bgcolor="#c7efce">
|
||
<td>time-a.nist.gov</td>
|
||
<td>129.6.15.28</td>
|
||
<td>NIST, Gaithersburg, Maryland</td>
|
||
</tr>
|
||
<tr align="left" valign="top">
|
||
<td>time-b.nist.gov</td>
|
||
<td>129.6.15.29</td>
|
||
<td>NIST, Gaithersburg, Maryland</td>
|
||
</tr>
|
||
<tr align="left" valign="top" bgcolor="#c7efce">
|
||
<td>time-a.timefreq.bldrdoc.gov</td>
|
||
<td>132.163.4.101</td>
|
||
<td>NIST, Boulder, Colorado</td>
|
||
</tr>
|
||
<tr align="left" valign="top">
|
||
<td>time-b.timefreq.bldrdoc.gov</td>
|
||
<td>132.163.4.102</td>
|
||
<td>NIST, Boulder, Colorado</td>
|
||
</tr>
|
||
<tr align="left" valign="top" bgcolor="#c7efce">
|
||
<td>time-c.timefreq.bldrdoc.gov</td>
|
||
<td>132.163.4.103</td>
|
||
<td>NIST, Boulder, Colorado</td>
|
||
</tr>
|
||
</table>
|
||
"""
|
||
integer = pp.Word(pp.nums)
|
||
ipAddress = pp.Combine(integer + "." + integer + "." + integer + "." + integer)
|
||
tdStart = pp.Suppress("<td>")
|
||
tdEnd = pp.Suppress("</td>")
|
||
timeServerPattern = (
|
||
tdStart
|
||
+ ipAddress("ipAddr")
|
||
+ tdEnd
|
||
+ tdStart
|
||
+ pp.CharsNotIn("<")("loc")
|
||
+ tdEnd
|
||
)
|
||
servers = [
|
||
srvr.ipAddr
|
||
for srvr, startloc, endloc in timeServerPattern.scan_string(testdata)
|
||
]
|
||
|
||
print(servers)
|
||
self.assertEqual(
|
||
[
|
||
"129.6.15.28",
|
||
"129.6.15.29",
|
||
"132.163.4.101",
|
||
"132.163.4.102",
|
||
"132.163.4.103",
|
||
],
|
||
servers,
|
||
"failed scan_string()",
|
||
)
|
||
|
||
servers = [
|
||
srvr.ipAddr
|
||
for srvr, startloc, endloc in timeServerPattern.scan_string(
|
||
testdata, max_matches=3
|
||
)
|
||
]
|
||
|
||
self.assertEqual(
|
||
[
|
||
"129.6.15.28",
|
||
"129.6.15.29",
|
||
"132.163.4.101",
|
||
],
|
||
servers,
|
||
"failed scan_string() with max_matches=3",
|
||
)
|
||
|
||
# test for string_end detection in scan_string
|
||
foundStringEnds = [r for r in pp.StringEnd().scan_string("xyzzy")]
|
||
print(foundStringEnds)
|
||
self.assertTrue(foundStringEnds, "Failed to find StringEnd in scan_string")
|
||
|
||
def testQuotedStrings(self):
|
||
testData = """
|
||
'a valid single quoted string'
|
||
'an invalid single quoted string
|
||
because it spans lines'
|
||
"a valid double quoted string"
|
||
"an invalid double quoted string
|
||
because it spans lines"
|
||
"""
|
||
print(testData)
|
||
|
||
with self.subTest():
|
||
sglStrings = [
|
||
(t[0], b, e) for (t, b, e) in pp.sgl_quoted_string.scan_string(testData)
|
||
]
|
||
print(sglStrings)
|
||
self.assertTrue(
|
||
len(sglStrings) == 1
|
||
and (sglStrings[0][1] == 17 and sglStrings[0][2] == 47),
|
||
"single quoted string failure",
|
||
)
|
||
|
||
with self.subTest():
|
||
dblStrings = [
|
||
(t[0], b, e) for (t, b, e) in pp.dbl_quoted_string.scan_string(testData)
|
||
]
|
||
print(dblStrings)
|
||
self.assertTrue(
|
||
len(dblStrings) == 1
|
||
and (dblStrings[0][1] == 154 and dblStrings[0][2] == 184),
|
||
"double quoted string failure",
|
||
)
|
||
|
||
with self.subTest():
|
||
allStrings = [
|
||
(t[0], b, e) for (t, b, e) in pp.quoted_string.scan_string(testData)
|
||
]
|
||
print(allStrings)
|
||
self.assertTrue(
|
||
len(allStrings) == 2
|
||
and (allStrings[0][1] == 17 and allStrings[0][2] == 47)
|
||
and (allStrings[1][1] == 154 and allStrings[1][2] == 184),
|
||
"quoted string failure",
|
||
)
|
||
|
||
escapedQuoteTest = r"""
|
||
'This string has an escaped (\') quote character'
|
||
"This string has an escaped (\") quote character"
|
||
"""
|
||
|
||
with self.subTest():
|
||
sglStrings = [
|
||
(t[0], b, e)
|
||
for (t, b, e) in pp.sgl_quoted_string.scan_string(escapedQuoteTest)
|
||
]
|
||
print(sglStrings)
|
||
self.assertTrue(
|
||
len(sglStrings) == 1
|
||
and (sglStrings[0][1] == 17 and sglStrings[0][2] == 66),
|
||
f"single quoted string escaped quote failure ({sglStrings[0]})",
|
||
)
|
||
|
||
with self.subTest():
|
||
dblStrings = [
|
||
(t[0], b, e)
|
||
for (t, b, e) in pp.dbl_quoted_string.scan_string(escapedQuoteTest)
|
||
]
|
||
print(dblStrings)
|
||
self.assertTrue(
|
||
len(dblStrings) == 1
|
||
and (dblStrings[0][1] == 83 and dblStrings[0][2] == 132),
|
||
f"double quoted string escaped quote failure ({dblStrings[0]})",
|
||
)
|
||
|
||
with self.subTest():
|
||
allStrings = [
|
||
(t[0], b, e)
|
||
for (t, b, e) in pp.quoted_string.scan_string(escapedQuoteTest)
|
||
]
|
||
print(allStrings)
|
||
self.assertTrue(
|
||
len(allStrings) == 2
|
||
and (
|
||
allStrings[0][1] == 17
|
||
and allStrings[0][2] == 66
|
||
and allStrings[1][1] == 83
|
||
and allStrings[1][2] == 132
|
||
),
|
||
f"quoted string escaped quote failure ({[str(s[0]) for s in allStrings]})",
|
||
)
|
||
|
||
dblQuoteTest = r"""
|
||
'This string has an doubled ('') quote character'
|
||
"This string has an doubled ("") quote character"
|
||
"""
|
||
with self.subTest():
|
||
sglStrings = [
|
||
(t[0], b, e)
|
||
for (t, b, e) in pp.sgl_quoted_string.scan_string(dblQuoteTest)
|
||
]
|
||
print(sglStrings)
|
||
self.assertTrue(
|
||
len(sglStrings) == 1
|
||
and (sglStrings[0][1] == 17 and sglStrings[0][2] == 66),
|
||
f"single quoted string escaped quote failure ({sglStrings[0]})",
|
||
)
|
||
|
||
with self.subTest():
|
||
dblStrings = [
|
||
(t[0], b, e)
|
||
for (t, b, e) in pp.dbl_quoted_string.scan_string(dblQuoteTest)
|
||
]
|
||
print(dblStrings)
|
||
self.assertTrue(
|
||
len(dblStrings) == 1
|
||
and (dblStrings[0][1] == 83 and dblStrings[0][2] == 132),
|
||
f"double quoted string escaped quote failure ({dblStrings[0]})",
|
||
)
|
||
|
||
with self.subTest():
|
||
allStrings = [
|
||
(t[0], b, e) for (t, b, e) in pp.quoted_string.scan_string(dblQuoteTest)
|
||
]
|
||
print(allStrings)
|
||
self.assertTrue(
|
||
len(allStrings) == 2
|
||
and (
|
||
allStrings[0][1] == 17
|
||
and allStrings[0][2] == 66
|
||
and allStrings[1][1] == 83
|
||
and allStrings[1][2] == 132
|
||
),
|
||
f"quoted string escaped quote failure ({[str(s[0]) for s in allStrings]})",
|
||
)
|
||
|
||
# test invalid end_quote_char
|
||
with self.subTest():
|
||
with self.assertRaises(
|
||
ValueError, msg="issue raising error for invalid end_quote_char"
|
||
):
|
||
expr = pp.QuotedString('"', end_quote_char=" ")
|
||
|
||
with self.subTest():
|
||
source = """
|
||
'''
|
||
multiline quote with comment # this is a comment
|
||
'''
|
||
\"\"\"
|
||
multiline quote with comment # this is a comment
|
||
\"\"\"
|
||
"single line quote with comment # this is a comment"
|
||
'single line quote with comment # this is a comment'
|
||
"""
|
||
stripped = (
|
||
pp.python_style_comment.ignore(pp.python_quoted_string)
|
||
.suppress()
|
||
.transform_string(source)
|
||
)
|
||
self.assertEqual(source, stripped)
|
||
|
||
def testQuotedStringUnquotesAndConvertWhitespaceEscapes(self):
|
||
# test for Issue #474
|
||
# fmt: off
|
||
backslash = chr(92) # a single backslash
|
||
tab = "\t"
|
||
newline = "\n"
|
||
test_string_0 = f'"{backslash}{backslash}n"' # r"\\n"
|
||
test_string_1 = f'"{backslash}t{backslash}{backslash}n"' # r"\t\\n"
|
||
test_string_2 = f'"a{backslash}tb"' # r"a\tb"
|
||
test_string_3 = f'"{backslash}{backslash}{backslash}n"' # r"\\\n"
|
||
T, F = True, False # these make the test cases format nicely
|
||
for test_parameters in (
|
||
# Parameters are the arguments to creating a QuotedString
|
||
# and the expected parsed list of characters):
|
||
# - unquote_results
|
||
# - convert_whitespace_escapes
|
||
# - test string
|
||
# - expected parsed characters (broken out as separate
|
||
# list items (all those doubled backslashes make it
|
||
# difficult to interpret the output)
|
||
(T, T, test_string_0, [backslash, "n"]),
|
||
(T, F, test_string_0, [backslash, "n"]),
|
||
(F, F, test_string_0, ['"', backslash, backslash, "n", '"']),
|
||
(T, T, test_string_1, [tab, backslash, "n"]),
|
||
(T, F, test_string_1, ["t", backslash, "n"]),
|
||
(F, F, test_string_1, ['"', backslash, "t", backslash, backslash, "n", '"']),
|
||
(T, T, test_string_2, ["a", tab, "b"]),
|
||
(T, F, test_string_2, ["a", "t", "b"]),
|
||
(F, F, test_string_2, ['"', "a", backslash, "t", "b", '"']),
|
||
(T, T, test_string_3, [backslash, newline]),
|
||
(T, F, test_string_3, [backslash, "n"]),
|
||
(F, F, test_string_3, ['"', backslash, backslash, backslash, "n", '"']),
|
||
):
|
||
unquote_results, convert_ws_escapes, test_string, expected_list = test_parameters
|
||
test_description = f"Testing with parameters {test_parameters}"
|
||
with self.subTest(msg=test_description):
|
||
print(test_description)
|
||
print(f"unquote_results: {unquote_results}"
|
||
f"\nconvert_whitespace_escapes: {convert_ws_escapes}")
|
||
qs_expr = pp.QuotedString(
|
||
quote_char='"',
|
||
esc_char='\\',
|
||
unquote_results=unquote_results,
|
||
convert_whitespace_escapes=convert_ws_escapes
|
||
)
|
||
result = qs_expr.parse_string(test_string)
|
||
|
||
# do this instead of assertParserAndCheckList to explicitly
|
||
# check and display the separate items in the list
|
||
print("Results:")
|
||
control_chars = {newline: "<NEWLINE>", backslash: "<BACKSLASH>", tab: "<TAB>"}
|
||
print(f"[{', '.join(control_chars.get(c, repr(c)) for c in result[0])}]")
|
||
self.assertEqual(expected_list, list(result[0]))
|
||
|
||
print()
|
||
# fmt: on
|
||
|
||
def testPythonQuotedStrings(self):
|
||
# fmt: off
|
||
success1, _ = pp.python_quoted_string.run_tests([
|
||
'"""xyz"""',
|
||
'''"""xyz
|
||
"""''',
|
||
'"""xyz "" """',
|
||
'''"""xyz ""
|
||
"""''',
|
||
'"""xyz " """',
|
||
'''"""xyz "
|
||
"""''',
|
||
r'''"""xyz \"""
|
||
|
||
"""''',
|
||
"'''xyz'''",
|
||
"""'''xyz
|
||
'''""",
|
||
"'''xyz '' '''",
|
||
"""'''xyz ''
|
||
'''""",
|
||
"'''xyz ' '''",
|
||
"""'''xyz '
|
||
'''""",
|
||
r"""'''xyz \'''
|
||
'''""",
|
||
])
|
||
|
||
print("\n\nFailure tests")
|
||
success2, _ = pp.python_quoted_string.run_tests([
|
||
'"xyz"""',
|
||
], failure_tests=True)
|
||
|
||
self.assertTrue(success1 and success2, "Python quoted string matching failure")
|
||
# fmt: on
|
||
|
||
def testCaselessOneOf(self):
|
||
caseless1 = pp.one_of("d a b c aA B A C", caseless=True)
|
||
caseless1str = str(caseless1)
|
||
print(caseless1str)
|
||
caseless2 = pp.one_of("d a b c Aa B A C", caseless=True)
|
||
caseless2str = str(caseless2)
|
||
print(caseless2str)
|
||
self.assertEqual(
|
||
caseless1str.upper(),
|
||
caseless2str.upper(),
|
||
"one_of not handling caseless option properly",
|
||
)
|
||
self.assertNotEqual(
|
||
caseless1str, caseless2str, "Caseless option properly sorted"
|
||
)
|
||
|
||
res = caseless1[...].parse_string("AAaaAaaA", parse_all=True)
|
||
print(res)
|
||
self.assertEqual(4, len(res), "caseless1 one_of failed")
|
||
self.assertEqual(
|
||
"aA" * 4, "".join(res), "caseless1 CaselessLiteral return failed"
|
||
)
|
||
|
||
res = caseless2[...].parse_string("AAaaAaaA", parse_all=True)
|
||
print(res)
|
||
self.assertEqual(4, len(res), "caseless2 one_of failed")
|
||
self.assertEqual(
|
||
"Aa" * 4, "".join(res), "caseless1 CaselessLiteral return failed"
|
||
)
|
||
|
||
def testCStyleCommentParser(self):
|
||
print("verify processing of C-style /* */ comments")
|
||
testdata = f"""
|
||
/* */
|
||
/** **/
|
||
/**/
|
||
/*{'*' * 1_000_000}*/
|
||
/****/
|
||
/* /*/
|
||
/** /*/
|
||
/*** /*/
|
||
/*
|
||
ablsjdflj
|
||
*/
|
||
"""
|
||
for test_expr in (
|
||
pp.c_style_comment,
|
||
pp.cpp_style_comment,
|
||
pp.java_style_comment,
|
||
):
|
||
with self.subTest("parse test - /* */ comments", test_expr=test_expr):
|
||
found_matches = [
|
||
len(t[0]) for t, s, e in test_expr.scan_string(testdata)
|
||
]
|
||
self.assertEqual(
|
||
[5, 7, 4, 1000004, 6, 6, 7, 8, 33],
|
||
found_matches,
|
||
f"only found {test_expr} lengths {found_matches}",
|
||
)
|
||
|
||
found_lines = [
|
||
pp.lineno(s, testdata)
|
||
for t, s, e in test_expr.scan_string(testdata)
|
||
]
|
||
self.assertEqual(
|
||
[2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||
found_lines,
|
||
f"only found {test_expr} on lines {found_lines}",
|
||
)
|
||
|
||
def testHtmlCommentParser(self):
|
||
print("verify processing of HTML comments")
|
||
|
||
test_expr = pp.html_comment
|
||
testdata = """
|
||
<!-- -->
|
||
<!--- --->
|
||
<!---->
|
||
<!----->
|
||
<!------>
|
||
<!-- /-->
|
||
<!--- /-->
|
||
<!---- /-->
|
||
<!---- /- ->
|
||
<!---- / -- >
|
||
<!--
|
||
ablsjdflj
|
||
-->
|
||
"""
|
||
found_matches = [len(t[0]) for t, s, e in test_expr.scan_string(testdata)]
|
||
self.assertEqual(
|
||
[8, 10, 7, 8, 9, 9, 10, 11, 79],
|
||
found_matches,
|
||
f"only found {test_expr} lengths {found_matches}",
|
||
)
|
||
|
||
found_lines = [
|
||
pp.lineno(s, testdata) for t, s, e in pp.html_comment.scan_string(testdata)
|
||
]
|
||
self.assertEqual(
|
||
[2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||
found_lines,
|
||
f"only found HTML comments on lines {found_lines}",
|
||
)
|
||
|
||
def testDoubleSlashCommentParser(self):
|
||
print("verify processing of C++ and Java comments - // comments")
|
||
|
||
# test C++ single line comments that have line terminated with '\' (should continue comment to following line)
|
||
testdata = r"""
|
||
// comment1
|
||
// comment2 \
|
||
still comment 2
|
||
// comment 3
|
||
"""
|
||
for test_expr in (
|
||
pp.dbl_slash_comment,
|
||
pp.cpp_style_comment,
|
||
pp.java_style_comment,
|
||
):
|
||
with self.subTest("parse test - // comments", test_expr=test_expr):
|
||
found_matches = [
|
||
len(t[0]) for t, s, e in test_expr.scan_string(testdata)
|
||
]
|
||
self.assertEqual(
|
||
[11, 41, 12],
|
||
found_matches,
|
||
f"only found {test_expr} lengths {found_matches}",
|
||
)
|
||
|
||
found_lines = [
|
||
pp.lineno(s, testdata)
|
||
for t, s, e in test_expr.scan_string(testdata)
|
||
]
|
||
self.assertEqual(
|
||
[2, 3, 5],
|
||
found_lines,
|
||
f"only found {test_expr} on lines {found_lines}",
|
||
)
|
||
|
||
def testReCatastrophicBacktrackingInQuotedStringParsers(self):
|
||
# reported by webpentest - 2016-04-28
|
||
print(
|
||
"testing catastrophic RE backtracking in implementation of quoted string parsers"
|
||
)
|
||
repeat = 5000
|
||
for expr, test_string in [
|
||
(pp.dblQuotedString, '"' + "\xff" * repeat),
|
||
(pp.sglQuotedString, "'" + "\xff" * repeat),
|
||
(pp.quotedString, '"' + "\xff" * repeat),
|
||
(pp.quotedString, "'" + "\xff" * repeat),
|
||
(pp.QuotedString('"'), '"' + "\xff" * repeat),
|
||
(pp.QuotedString("'"), "'" + "\xff" * repeat),
|
||
]:
|
||
test_string_label = f"{test_string[:2]}..."
|
||
with self.subTest(expr=expr, test_string=repr(test_string_label)):
|
||
# parse a valid quoted string
|
||
expr.parse_string(test_string + test_string[0], parse_all=True)
|
||
|
||
# try to parse a quoted string with no trailing quote
|
||
with self.assertRaisesParseException():
|
||
expr.parse_string(test_string, parse_all=True)
|
||
|
||
def testReCatastrophicBacktrackingInCommentParsers(self):
|
||
print(
|
||
"testing catastrophic RE backtracking in implementation of comment parsers"
|
||
)
|
||
for expr, test_string in [
|
||
(pp.c_style_comment, f"/*{'*' * 500}"),
|
||
(pp.cpp_style_comment, f"/*{'*' * 500}"),
|
||
(pp.java_style_comment, f"/*{'*' * 500}"),
|
||
(pp.html_comment, f"<-- {'-' * 500}"),
|
||
]:
|
||
with self.subTest("Test catastrophic RE backtracking", expr=expr):
|
||
try:
|
||
expr.parse_string(test_string)
|
||
except pp.ParseException:
|
||
continue
|
||
|
||
def testParseExpressionResults(self):
|
||
a = pp.Word("a", pp.alphas).set_name("A")
|
||
b = pp.Word("b", pp.alphas).set_name("B")
|
||
c = pp.Word("c", pp.alphas).set_name("C")
|
||
ab = (a + b).set_name("AB")
|
||
abc = (ab + c).set_name("ABC")
|
||
word = pp.Word(pp.alphas).set_name("word")
|
||
|
||
words = pp.Group(pp.OneOrMore(~a + word)).set_name("words")
|
||
|
||
phrase = (
|
||
words("Head")
|
||
+ pp.Group(a + pp.Optional(b + pp.Optional(c)))("ABC")
|
||
+ words("Tail")
|
||
)
|
||
|
||
results = phrase.parse_string(
|
||
"xavier yeti alpha beta charlie will beaver", parse_all=True
|
||
)
|
||
print(results, results.Head, results.ABC, results.Tail)
|
||
for key, ln in [("Head", 2), ("ABC", 3), ("Tail", 2)]:
|
||
self.assertEqual(
|
||
ln,
|
||
len(results[key]),
|
||
f"expected {ln:d} elements in {key}, found {results[key]}",
|
||
)
|
||
|
||
def testParseKeyword(self):
|
||
kw = pp.Keyword("if")
|
||
lit = pp.Literal("if")
|
||
|
||
def test(s, litShouldPass, kwShouldPass):
|
||
print("Test", s)
|
||
print("Match Literal", end=" ")
|
||
try:
|
||
print(lit.parse_string(s, parse_all=False))
|
||
except Exception:
|
||
print("failed")
|
||
if litShouldPass:
|
||
self.fail(f"Literal failed to match {s}, should have")
|
||
else:
|
||
if not litShouldPass:
|
||
self.fail(f"Literal matched {s}, should not have")
|
||
|
||
print("Match Keyword", end=" ")
|
||
try:
|
||
print(kw.parse_string(s, parse_all=False))
|
||
except Exception:
|
||
print("failed")
|
||
if kwShouldPass:
|
||
self.fail(f"Keyword failed to match {s}, should have")
|
||
else:
|
||
if not kwShouldPass:
|
||
self.fail(f"Keyword matched {s}, should not have")
|
||
|
||
test("ifOnlyIfOnly", True, False)
|
||
test("if(OnlyIfOnly)", True, True)
|
||
test("if (OnlyIf Only)", True, True)
|
||
|
||
kw = pp.Keyword("if", caseless=True)
|
||
|
||
test("IFOnlyIfOnly", False, False)
|
||
test("If(OnlyIfOnly)", False, True)
|
||
test("iF (OnlyIf Only)", False, True)
|
||
|
||
with self.assertRaises(
|
||
ValueError, msg="failed to warn empty string passed to Keyword"
|
||
):
|
||
kw = pp.Keyword("")
|
||
|
||
def testParseExpressionResultsAccumulate(self):
|
||
num = pp.Word(pp.nums).set_name("num")("base10*")
|
||
hexnum = pp.Combine("0x" + pp.Word(pp.nums)).set_name("hexnum")("hex*")
|
||
name = pp.Word(pp.alphas).set_name("word")("word*")
|
||
list_of_num = pp.DelimitedList(hexnum | num | name, ",")
|
||
|
||
tokens = list_of_num.parse_string("1, 0x2, 3, 0x4, aaa", parse_all=True)
|
||
print(tokens.dump())
|
||
self.assertParseResultsEquals(
|
||
tokens,
|
||
expected_list=["1", "0x2", "3", "0x4", "aaa"],
|
||
expected_dict={
|
||
"base10": ["1", "3"],
|
||
"hex": ["0x2", "0x4"],
|
||
"word": ["aaa"],
|
||
},
|
||
)
|
||
|
||
lbrack = pp.Literal("(").suppress()
|
||
rbrack = pp.Literal(")").suppress()
|
||
integer = pp.Word(pp.nums).set_name("int")
|
||
variable = pp.Word(pp.alphas, max=1).set_name("variable")
|
||
relation_body_item = (
|
||
variable | integer | pp.quoted_string().set_parse_action(pp.remove_quotes)
|
||
)
|
||
relation_name = pp.Word(pp.alphas + "_", pp.alphanums + "_")
|
||
relation_body = lbrack + pp.Group(pp.DelimitedList(relation_body_item)) + rbrack
|
||
Goal = pp.Dict(pp.Group(relation_name + relation_body))
|
||
Comparison_Predicate = pp.Group(variable + pp.one_of("< >") + integer)("pred*")
|
||
Query = Goal("head") + ":-" + pp.DelimitedList(Goal | Comparison_Predicate)
|
||
|
||
test = """Q(x,y,z):-Bloo(x,"Mitsis",y),Foo(y,z,1243),y>28,x<12,x>3"""
|
||
|
||
queryRes = Query.parse_string(test, parse_all=True)
|
||
print(queryRes.dump())
|
||
self.assertParseResultsEquals(
|
||
queryRes.pred,
|
||
expected_list=[["y", ">", "28"], ["x", "<", "12"], ["x", ">", "3"]],
|
||
msg=f"Incorrect list for attribute pred, {queryRes.pred.as_list()}",
|
||
)
|
||
|
||
def testReStringRange(self):
|
||
testCases = (
|
||
r"[A-Z]",
|
||
r"[A-A]",
|
||
r"[A-Za-z]",
|
||
r"[A-z]",
|
||
r"[\ -\~]",
|
||
r"[\0x20-0]",
|
||
r"[\0x21-\0x7E]",
|
||
r"[\0xa1-\0xfe]",
|
||
r"[\040-0]",
|
||
r"[A-Za-z0-9]",
|
||
r"[A-Za-z0-9_]",
|
||
r"[A-Za-z0-9_$]",
|
||
r"[A-Za-z0-9_$\-]",
|
||
r"[^0-9\\]",
|
||
r"[a-zA-Z]",
|
||
r"[/\^~]",
|
||
r"[=\+\-!]",
|
||
r"[A-]",
|
||
r"[-A]",
|
||
r"[\x21]",
|
||
r"[а-яА-ЯёЁA-Z$_\041α-ω]",
|
||
r"[\0xc0-\0xd6\0xd8-\0xf6\0xf8-\0xff]",
|
||
r"[\0xa1-\0xbf\0xd7\0xf7]",
|
||
r"[\0xc0-\0xd6\0xd8-\0xf6\0xf8-\0xff]",
|
||
r"[\0xa1-\0xbf\0xd7\0xf7]",
|
||
r"[\\[\]\/\-\*\.\$\+\^\?()~ ]",
|
||
)
|
||
expectedResults = (
|
||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||
"A",
|
||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz",
|
||
" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~",
|
||
" !\"#$%&'()*+,-./0",
|
||
"!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~",
|
||
"¡¢£¤¥¦§¨©ª«¬®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ",
|
||
" !\"#$%&'()*+,-./0",
|
||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
|
||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_",
|
||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_$",
|
||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_$-",
|
||
"0123456789\\",
|
||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||
"/^~",
|
||
"=+-!",
|
||
"A-",
|
||
"-A",
|
||
"!",
|
||
"абвгдежзийклмнопрстуфхцчшщъыьэюяАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯёЁABCDEFGHIJKLMNOPQRSTUVWXYZ$_!αβγδεζηθικλμνξοπρςστυφχψω",
|
||
"ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ",
|
||
"¡¢£¤¥¦§¨©ª«¬\xad®¯°±²³´µ¶·¸¹º»¼½¾¿×÷",
|
||
pp.alphas8bit,
|
||
pp.punc8bit,
|
||
r"\[]/-*.$+^?()~ ",
|
||
)
|
||
for test in zip(testCases, expectedResults):
|
||
t, exp = test
|
||
res = pp.srange(t)
|
||
# print(t, "->", res)
|
||
self.assertEqual(
|
||
exp,
|
||
res,
|
||
f"srange error, srange({t!r})->'{res!r}', expected '{exp!r}'",
|
||
)
|
||
|
||
def testSkipToParserTests(self):
|
||
thingToFind = pp.Literal("working")
|
||
testExpr = (
|
||
pp.SkipTo(pp.Literal(";"), include=True, ignore=pp.c_style_comment)
|
||
+ thingToFind
|
||
)
|
||
|
||
def test_parse(someText):
|
||
print(testExpr.parse_string(someText, parse_all=True))
|
||
|
||
# This first test works, as the SkipTo expression is immediately following the ignore expression (c_style_comment)
|
||
test_parse("some text /* comment with ; in */; working")
|
||
# This second test previously failed, as there is text following the ignore expression, and before the SkipTo expression.
|
||
test_parse("some text /* comment with ; in */some other stuff; working")
|
||
|
||
# tests for optional fail_on argument
|
||
testExpr = (
|
||
pp.SkipTo(
|
||
pp.Literal(";"),
|
||
include=True,
|
||
ignore=pp.c_style_comment,
|
||
fail_on="other",
|
||
)
|
||
+ thingToFind
|
||
)
|
||
test_parse("some text /* comment with ; in */; working")
|
||
|
||
with self.assertRaisesParseException():
|
||
test_parse("some text /* comment with ; in */some other stuff; working")
|
||
|
||
# test that we correctly create named results
|
||
text = "prefixDATAsuffix"
|
||
data = pp.Literal("DATA")
|
||
suffix = pp.Literal("suffix")
|
||
expr = pp.SkipTo(data + suffix)("prefix") + data + suffix
|
||
result = expr.parse_string(text, parse_all=True)
|
||
self.assertTrue(
|
||
isinstance(result.prefix, str),
|
||
"SkipTo created with wrong saveAsList attribute",
|
||
)
|
||
|
||
alpha_word = (
|
||
~pp.Literal("end") + pp.Word(pp.alphas, as_keyword=True)
|
||
).set_name("alpha")
|
||
num_word = pp.Word(pp.nums, as_keyword=True).set_name("int")
|
||
|
||
def test(expr, test_string, expected_list, expected_dict):
|
||
if (expected_list, expected_dict) == (None, None):
|
||
with self.assertRaises(
|
||
Exception, msg=f"{expr} failed to parse {test_string!r}"
|
||
):
|
||
expr.parse_string(test_string, parse_all=True)
|
||
else:
|
||
result = expr.parse_string(test_string, parse_all=True)
|
||
self.assertParseResultsEquals(
|
||
result, expected_list=expected_list, expected_dict=expected_dict
|
||
)
|
||
|
||
# ellipses for SkipTo
|
||
e = ... + pp.Literal("end")
|
||
test(e, "start 123 end", ["start 123 ", "end"], {"_skipped": ["start 123 "]})
|
||
|
||
e = pp.Suppress(...) + pp.Literal("end")
|
||
test(e, "start 123 end", ["end"], {})
|
||
|
||
e = pp.Literal("start") + ... + pp.Literal("end")
|
||
test(e, "start 123 end", ["start", "123 ", "end"], {"_skipped": ["123 "]})
|
||
|
||
e = ... + pp.Literal("middle") + ... + pp.Literal("end")
|
||
test(
|
||
e,
|
||
"start 123 middle 456 end",
|
||
["start 123 ", "middle", "456 ", "end"],
|
||
{"_skipped": ["start 123 ", "456 "]},
|
||
)
|
||
|
||
e = pp.Suppress(...) + pp.Literal("middle") + ... + pp.Literal("end")
|
||
test(
|
||
e,
|
||
"start 123 middle 456 end",
|
||
["middle", "456 ", "end"],
|
||
{"_skipped": ["456 "]},
|
||
)
|
||
|
||
e = pp.Literal("start") + ...
|
||
test(e, "start 123 end", None, None)
|
||
|
||
e = pp.And(["start", ..., "end"])
|
||
test(e, "start 123 end", ["start", "123 ", "end"], {"_skipped": ["123 "]})
|
||
|
||
e = pp.And([..., "end"])
|
||
test(e, "start 123 end", ["start 123 ", "end"], {"_skipped": ["start 123 "]})
|
||
|
||
e = "start" + (num_word | ...) + "end"
|
||
test(e, "start 456 end", ["start", "456", "end"], {})
|
||
test(
|
||
e,
|
||
"start 123 456 end",
|
||
["start", "123", "456 ", "end"],
|
||
{"_skipped": ["456 "]},
|
||
)
|
||
test(e, "start end", ["start", "", "end"], {"_skipped": ["missing <int>"]})
|
||
|
||
# e = define_expr('"start" + (num_word | ...)("inner") + "end"')
|
||
# test(e, "start 456 end", ['start', '456', 'end'], {'inner': '456'})
|
||
|
||
e = "start" + (alpha_word[...] & num_word[...] | ...) + "end"
|
||
test(e, "start 456 red end", ["start", "456", "red", "end"], {})
|
||
test(e, "start red 456 end", ["start", "red", "456", "end"], {})
|
||
test(
|
||
e,
|
||
"start 456 red + end",
|
||
["start", "456", "red", "+ ", "end"],
|
||
{"_skipped": ["+ "]},
|
||
)
|
||
test(e, "start red end", ["start", "red", "end"], {})
|
||
test(e, "start 456 end", ["start", "456", "end"], {})
|
||
test(e, "start end", ["start", "end"], {})
|
||
test(e, "start 456 + end", ["start", "456", "+ ", "end"], {"_skipped": ["+ "]})
|
||
|
||
e = "start" + (alpha_word[1, ...] & num_word[1, ...] | ...) + "end"
|
||
test(e, "start 456 red end", ["start", "456", "red", "end"], {})
|
||
test(e, "start red 456 end", ["start", "red", "456", "end"], {})
|
||
test(
|
||
e,
|
||
"start 456 red + end",
|
||
["start", "456", "red", "+ ", "end"],
|
||
{"_skipped": ["+ "]},
|
||
)
|
||
test(e, "start red end", ["start", "red ", "end"], {"_skipped": ["red "]})
|
||
test(e, "start 456 end", ["start", "456 ", "end"], {"_skipped": ["456 "]})
|
||
test(
|
||
e,
|
||
"start end",
|
||
["start", "", "end"],
|
||
{"_skipped": ["missing <{{alpha}... & {int}...}>"]},
|
||
)
|
||
test(e, "start 456 + end", ["start", "456 + ", "end"], {"_skipped": ["456 + "]})
|
||
|
||
e = "start" + (alpha_word | ...) + (num_word | ...) + "end"
|
||
test(e, "start red 456 end", ["start", "red", "456", "end"], {})
|
||
test(
|
||
e,
|
||
"start red end",
|
||
["start", "red", "", "end"],
|
||
{"_skipped": ["missing <int>"]},
|
||
)
|
||
test(
|
||
e,
|
||
"start 456 end",
|
||
["start", "", "456", "end"],
|
||
{"_skipped": ["missing <alpha>"]},
|
||
)
|
||
test(
|
||
e,
|
||
"start end",
|
||
["start", "", "", "end"],
|
||
{"_skipped": ["missing <alpha>", "missing <int>"]},
|
||
)
|
||
|
||
e = pp.Literal("start") + ... + "+" + ... + "end"
|
||
test(
|
||
e,
|
||
"start red + 456 end",
|
||
["start", "red ", "+", "456 ", "end"],
|
||
{"_skipped": ["red ", "456 "]},
|
||
)
|
||
|
||
def testSkipToPreParseIgnoreExprs(self):
|
||
# added to verify fix to Issue #475
|
||
from pyparsing import Word, alphanums, python_style_comment
|
||
|
||
some_grammar = Word(alphanums) + ":=" + ... + ";"
|
||
some_grammar.ignore(python_style_comment)
|
||
try:
|
||
result = some_grammar.parse_string(
|
||
"""\
|
||
var1 := 2 # 3; <== this semi-colon will match!
|
||
+ 1;
|
||
""",
|
||
parse_all=True,
|
||
)
|
||
except ParseException as pe:
|
||
print(pe.explain())
|
||
raise
|
||
else:
|
||
print(result.dump())
|
||
|
||
def testSkipToIgnoreExpr2(self):
|
||
a, star = pp.Literal.using_each("a*")
|
||
wrapper = a + ... + a
|
||
expr = star + pp.SkipTo(star, ignore=wrapper) + star
|
||
|
||
# pyparsing 3.0.9 -> ['*', 'a_*_a', '*']
|
||
# pyparsing 3.1.0 -> ['*', '', '*']
|
||
self.assertParseAndCheckList(expr, "*a_*_a*", ["*", "a_*_a", "*"])
|
||
|
||
def testEllipsisRepetition(self):
|
||
word = pp.Word(pp.alphas).set_name("word")
|
||
num = pp.Word(pp.nums).set_name("num")
|
||
|
||
exprs = [
|
||
word[...] + num,
|
||
word * ... + num,
|
||
word[0, ...] + num,
|
||
word[1, ...] + num,
|
||
word[2, ...] + num,
|
||
word[..., 3] + num,
|
||
word[2] + num,
|
||
]
|
||
|
||
expected_res = [
|
||
r"([abcd]+ )*\d+",
|
||
r"([abcd]+ )*\d+",
|
||
r"([abcd]+ )*\d+",
|
||
r"([abcd]+ )+\d+",
|
||
r"([abcd]+ ){2,}\d+",
|
||
r"([abcd]+ ){0,3}\d+",
|
||
r"([abcd]+ ){2}\d+",
|
||
]
|
||
|
||
tests = ["aa bb cc dd 123", "bb cc dd 123", "cc dd 123", "dd 123", "123"]
|
||
|
||
all_success = True
|
||
for expr, expected_re in zip(exprs, expected_res):
|
||
successful_tests = [t for t in tests if re.match(expected_re, t)]
|
||
failure_tests = [t for t in tests if not re.match(expected_re, t)]
|
||
success1, _ = expr.run_tests(successful_tests)
|
||
success2, _ = expr.run_tests(failure_tests, failure_tests=True)
|
||
all_success = all_success and success1 and success2
|
||
if not all_success:
|
||
print("Failed expression:", expr)
|
||
break
|
||
|
||
self.assertTrue(all_success, "failed getItem_ellipsis test")
|
||
|
||
def testEllipsisRepetitionWithResultsNames(self):
|
||
label = pp.Word(pp.alphas)
|
||
val = ppc.integer()
|
||
parser = label("label") + pp.ZeroOrMore(val)("values")
|
||
|
||
_, results = parser.run_tests(
|
||
"""
|
||
a 1
|
||
b 1 2 3
|
||
c
|
||
"""
|
||
)
|
||
expected = [
|
||
(["a", 1], {"label": "a", "values": [1]}),
|
||
(["b", 1, 2, 3], {"label": "b", "values": [1, 2, 3]}),
|
||
(["c"], {"label": "c", "values": []}),
|
||
]
|
||
for obs, exp in zip(results, expected):
|
||
test, result = obs
|
||
exp_list, exp_dict = exp
|
||
self.assertParseResultsEquals(
|
||
result, expected_list=exp_list, expected_dict=exp_dict
|
||
)
|
||
|
||
parser = label("label") + val[...]("values")
|
||
|
||
_, results = parser.run_tests(
|
||
"""
|
||
a 1
|
||
b 1 2 3
|
||
c
|
||
"""
|
||
)
|
||
expected = [
|
||
(["a", 1], {"label": "a", "values": [1]}),
|
||
(["b", 1, 2, 3], {"label": "b", "values": [1, 2, 3]}),
|
||
(["c"], {"label": "c", "values": []}),
|
||
]
|
||
for obs, exp in zip(results, expected):
|
||
test, result = obs
|
||
exp_list, exp_dict = exp
|
||
self.assertParseResultsEquals(
|
||
result, expected_list=exp_list, expected_dict=exp_dict
|
||
)
|
||
|
||
pt = pp.Group(val("x") + pp.Suppress(",") + val("y"))
|
||
parser = label("label") + pt[...]("points")
|
||
_, results = parser.run_tests(
|
||
"""
|
||
a 1,1
|
||
b 1,1 2,2 3,3
|
||
c
|
||
"""
|
||
)
|
||
expected = [
|
||
(["a", [1, 1]], {"label": "a", "points": [{"x": 1, "y": 1}]}),
|
||
(
|
||
["b", [1, 1], [2, 2], [3, 3]],
|
||
{
|
||
"label": "b",
|
||
"points": [{"x": 1, "y": 1}, {"x": 2, "y": 2}, {"x": 3, "y": 3}],
|
||
},
|
||
),
|
||
(["c"], {"label": "c", "points": []}),
|
||
]
|
||
for obs, exp in zip(results, expected):
|
||
test, result = obs
|
||
exp_list, exp_dict = exp
|
||
self.assertParseResultsEquals(
|
||
result, expected_list=exp_list, expected_dict=exp_dict
|
||
)
|
||
|
||
def testCustomQuotes(self):
|
||
testString = r"""
|
||
sdlfjs :sdf\:jls::djf: sl:kfsjf
|
||
sdlfjs -sdf\:jls::--djf: sl-kfsjf
|
||
sdlfjs -sdf\:::jls::--djf: sl:::-kfsjf
|
||
sdlfjs ^sdf\:jls^^--djf^ sl-kfsjf
|
||
sdlfjs ^^^==sdf\:j=lz::--djf: sl=^^=kfsjf
|
||
sdlfjs ==sdf\:j=ls::--djf: sl==kfsjf^^^
|
||
"""
|
||
print(testString)
|
||
|
||
colonQuotes = pp.QuotedString(":", "\\", "::")
|
||
dashQuotes = pp.QuotedString("-", "\\", "--")
|
||
hatQuotes = pp.QuotedString("^", "\\")
|
||
hatQuotes1 = pp.QuotedString("^", "\\", "^^")
|
||
dblEqQuotes = pp.QuotedString("==", "\\")
|
||
|
||
def test(label, quoteExpr, expected):
|
||
print(label)
|
||
print(quoteExpr.pattern)
|
||
print(quoteExpr.search_string(testString))
|
||
print(quoteExpr.search_string(testString)[0][0])
|
||
print(f"{expected}")
|
||
self.assertEqual(
|
||
expected,
|
||
quoteExpr.search_string(testString)[0][0],
|
||
f"failed to match {quoteExpr}, expected '{expected}', got '{quoteExpr.search_string(testString)[0]}'",
|
||
)
|
||
print()
|
||
|
||
test("colonQuotes", colonQuotes, r"sdf:jls:djf")
|
||
test("dashQuotes", dashQuotes, r"sdf:jls::-djf: sl")
|
||
test("hatQuotes", hatQuotes, r"sdf:jls")
|
||
test("hatQuotes1", hatQuotes1, r"sdf:jls^--djf")
|
||
test("dblEqQuotes", dblEqQuotes, r"sdf:j=ls::--djf: sl")
|
||
test("::: quotes", pp.QuotedString(":::"), "jls::--djf: sl")
|
||
test("==-- quotes", pp.QuotedString("==", end_quote_char="--"), r"sdf\:j=lz::")
|
||
test(
|
||
"^^^ multiline quotes",
|
||
pp.QuotedString("^^^", multiline=True),
|
||
r"""==sdf\:j=lz::--djf: sl=^^=kfsjf
|
||
sdlfjs ==sdf\:j=ls::--djf: sl==kfsjf""",
|
||
)
|
||
with self.assertRaises(ValueError):
|
||
pp.QuotedString("", "\\")
|
||
|
||
def testCustomQuotes2(self):
|
||
qs = pp.QuotedString(quote_char=".[", end_quote_char="].")
|
||
print(qs.reString)
|
||
self.assertParseAndCheckList(qs, ".[...].", ["..."])
|
||
self.assertParseAndCheckList(qs, ".[].", [""])
|
||
self.assertParseAndCheckList(qs, ".[]].", ["]"])
|
||
self.assertParseAndCheckList(qs, ".[]]].", ["]]"])
|
||
|
||
qs = pp.QuotedString(quote_char="+*", end_quote_char="*+")
|
||
print(qs.reString)
|
||
self.assertParseAndCheckList(qs, "+*...*+", ["..."])
|
||
self.assertParseAndCheckList(qs, "+**+", [""])
|
||
self.assertParseAndCheckList(qs, "+***+", ["*"])
|
||
self.assertParseAndCheckList(qs, "+****+", ["**"])
|
||
|
||
qs = pp.QuotedString(quote_char="*/", end_quote_char="/*")
|
||
print(qs.reString)
|
||
self.assertParseAndCheckList(qs, "*/.../*", ["..."])
|
||
self.assertParseAndCheckList(qs, "*//*", [""])
|
||
self.assertParseAndCheckList(qs, "*///*", ["/"])
|
||
self.assertParseAndCheckList(qs, "*////*", ["//"])
|
||
|
||
def testQuotedStringWithWhitespaceInQuoteChars(self):
|
||
# Issue #492 - a leading/trailing whitespace character that is part of a
|
||
# multi-character quote delimiter (such as a newline in "\n;") must not
|
||
# be stripped away, which previously collapsed the delimiter to ";".
|
||
with ppt.reset_pyparsing_context():
|
||
pp.ParserElement.set_default_whitespace_chars("")
|
||
newline_quote = pp.QuotedString("\n;", multiline=True)
|
||
self.assertParseAndCheckList(
|
||
newline_quote, "\n;Hi \n mum!\n;", ["Hi \n mum!"]
|
||
)
|
||
self.assertEqual(
|
||
[["Hi \n mum!"]],
|
||
newline_quote.search_string("lsjdf \n;Hi \n mum!\n; sldjf").as_list(),
|
||
)
|
||
self.assertEqual(
|
||
[["Hi \n m;um!"]],
|
||
newline_quote.search_string("lsjdf \n;Hi \n m;um!\n; sldjf").as_list(),
|
||
)
|
||
|
||
# a fully whitespace (or empty) quote_char is still rejected
|
||
for bad_quote in ("", " ", "\n"):
|
||
with self.assertRaises(ValueError):
|
||
pp.QuotedString(bad_quote)
|
||
with self.assertRaises(ValueError):
|
||
pp.QuotedString('"', end_quote_char=bad_quote)
|
||
|
||
def testRepeater(self):
|
||
if ParserElement._packratEnabled or ParserElement._left_recursion_enabled:
|
||
print("skipping this test, not compatible with memoization")
|
||
return
|
||
|
||
first = pp.Word("abcdef").set_name("word1")
|
||
bridge = pp.Word(pp.nums).set_name("number")
|
||
second = pp.match_previous_literal(first).set_name("repeat(word1Literal)")
|
||
|
||
seq = first + bridge + second
|
||
|
||
tests = [
|
||
("abc12abc", True),
|
||
("abc12aabc", False),
|
||
("abc12cba", True),
|
||
("abc12bca", True),
|
||
]
|
||
|
||
for tst, expected in tests:
|
||
found = False
|
||
for tokens, start, end in seq.scan_string(tst):
|
||
f, b, s = tokens
|
||
print(f, b, s)
|
||
found = True
|
||
if not found:
|
||
print("No literal match in", tst)
|
||
self.assertEqual(
|
||
expected,
|
||
found,
|
||
f"Failed repeater for test: {tst}, matching {seq}",
|
||
)
|
||
print()
|
||
|
||
# retest using match_previous_expr instead of match_previous_literal
|
||
second = pp.match_previous_expr(first).set_name("repeat(word1expr)")
|
||
seq = first + bridge + second
|
||
|
||
tests = [("abc12abc", True), ("abc12cba", False), ("abc12abcdef", False)]
|
||
|
||
for tst, expected in tests:
|
||
found = False
|
||
for tokens, start, end in seq.scan_string(tst):
|
||
print(tokens)
|
||
found = True
|
||
if not found:
|
||
print("No expression match in", tst)
|
||
self.assertEqual(
|
||
expected,
|
||
found,
|
||
f"Failed repeater for test: {tst}, matching {seq}",
|
||
)
|
||
|
||
print()
|
||
|
||
first = pp.Word("abcdef").set_name("word1")
|
||
bridge = pp.Word(pp.nums).set_name("number")
|
||
second = pp.match_previous_expr(first).set_name("repeat(word1)")
|
||
seq = first + bridge + second
|
||
csFirst = seq.set_name("word-num-word")
|
||
csSecond = pp.match_previous_expr(csFirst)
|
||
compoundSeq = csFirst + ":" + csSecond
|
||
compoundSeq.streamline()
|
||
print(compoundSeq)
|
||
|
||
tests = [
|
||
("abc12abc:abc12abc", True),
|
||
("abc12cba:abc12abc", False),
|
||
("abc12abc:abc12abcdef", False),
|
||
]
|
||
|
||
for tst, expected in tests:
|
||
found = False
|
||
for tokens, start, end in compoundSeq.scan_string(tst):
|
||
print("match:", tokens)
|
||
found = True
|
||
break
|
||
if not found:
|
||
print("No expression match in", tst)
|
||
self.assertEqual(
|
||
expected,
|
||
found,
|
||
f"Failed repeater for test: {tst}, matching {seq}",
|
||
)
|
||
|
||
print()
|
||
eFirst = pp.Word(pp.nums)
|
||
eSecond = pp.match_previous_expr(eFirst)
|
||
eSeq = eFirst + ":" + eSecond
|
||
|
||
tests = [("1:1A", True), ("1:10", False)]
|
||
|
||
for tst, expected in tests:
|
||
found = False
|
||
for tokens, start, end in eSeq.scan_string(tst):
|
||
print(tokens)
|
||
found = True
|
||
if not found:
|
||
print("No match in", tst)
|
||
self.assertEqual(
|
||
expected,
|
||
found,
|
||
f"Failed repeater for test: {tst}, matching {seq}",
|
||
)
|
||
|
||
def testRepeater2(self):
|
||
"""test match_previous_literal with empty repeater"""
|
||
|
||
if ParserElement._packratEnabled or ParserElement._left_recursion_enabled:
|
||
print("skipping this test, not compatible with memoization")
|
||
return
|
||
|
||
first = pp.Optional(pp.Word("abcdef").set_name("words1"))
|
||
bridge = pp.Word(pp.nums).set_name("number")
|
||
second = pp.match_previous_literal(first).set_name("repeat(word1Literal)")
|
||
|
||
seq = first + bridge + second
|
||
|
||
tst = "12"
|
||
expected = ["12"]
|
||
result = seq.parse_string(tst, parse_all=True)
|
||
print(result.dump())
|
||
|
||
self.assertParseResultsEquals(result, expected_list=expected)
|
||
|
||
def testRepeater3(self):
|
||
"""test match_previous_literal with multiple repeater tokens"""
|
||
|
||
if ParserElement._packratEnabled or ParserElement._left_recursion_enabled:
|
||
print("skipping this test, not compatible with memoization")
|
||
return
|
||
|
||
first = pp.Word("a") + pp.Word("d")
|
||
bridge = pp.Word(pp.nums).set_name("number")
|
||
second = pp.match_previous_literal(first) # ("second")
|
||
|
||
seq = first + bridge + second
|
||
|
||
tst = "aaaddd12aaaddd"
|
||
expected = ["aaa", "ddd", "12", "aaa", "ddd"]
|
||
result = seq.parse_string(tst, parse_all=True)
|
||
print(result.dump())
|
||
|
||
self.assertParseResultsEquals(result, expected_list=expected)
|
||
|
||
def testRepeater4(self):
|
||
"""test match_previous_expr with multiple repeater tokens"""
|
||
|
||
if ParserElement._packratEnabled or ParserElement._left_recursion_enabled:
|
||
print("skipping this test, not compatible with memoization")
|
||
return
|
||
|
||
first = pp.Group(pp.Word(pp.alphas) + pp.Word(pp.alphas))
|
||
bridge = pp.Word(pp.nums)
|
||
|
||
# no matching is used - this is just here for a sanity check
|
||
# second = pp.Group(pp.Word(pp.alphas) + pp.Word(pp.alphas))("second")
|
||
# second = pp.Group(pp.Word(pp.alphas) + pp.Word(pp.alphas)).set_results_name("second")
|
||
|
||
# ISSUE: when match_previous_expr returns multiple tokens the matching tokens are nested an extra level deep.
|
||
# This behavior is not seen with a single return token (see testRepeater5 directly below.)
|
||
second = pp.match_previous_expr(first)
|
||
|
||
expr = first + bridge.suppress() + second
|
||
|
||
tst = "aaa ddd 12 aaa ddd"
|
||
expected = [["aaa", "ddd"], ["aaa", "ddd"]]
|
||
result = expr.parse_string(tst, parse_all=True)
|
||
print(result.dump())
|
||
|
||
self.assertParseResultsEquals(result, expected_list=expected)
|
||
|
||
def testRepeater5(self):
|
||
"""a simplified testRepeater4 to examine match_previous_expr with a single repeater token"""
|
||
|
||
if ParserElement._packratEnabled or ParserElement._left_recursion_enabled:
|
||
print("skipping this test, not compatible with memoization")
|
||
return
|
||
|
||
first = pp.Word(pp.alphas)
|
||
bridge = pp.Word(pp.nums)
|
||
second = pp.match_previous_expr(first)
|
||
|
||
expr = first + bridge.suppress() + second
|
||
|
||
tst = "aaa 12 aaa"
|
||
expected = tst.replace("12", "").split()
|
||
result = expr.parse_string(tst, parse_all=True)
|
||
print(result.dump())
|
||
|
||
self.assertParseResultsEquals(result, expected_list=expected)
|
||
|
||
def testRecursiveCombine(self):
|
||
testInput = "myc(114)r(11)dd"
|
||
stream = pp.Forward()
|
||
stream <<= pp.Optional(pp.Word(pp.alphas)) + pp.Optional(
|
||
"(" + pp.Word(pp.nums) + ")" + stream
|
||
)
|
||
expected = ["".join(stream.parse_string(testInput, parse_all=True))]
|
||
print(expected)
|
||
|
||
stream = pp.Forward()
|
||
stream << pp.Combine(
|
||
pp.Optional(pp.Word(pp.alphas))
|
||
+ pp.Optional("(" + pp.Word(pp.nums) + ")" + stream)
|
||
)
|
||
testVal = stream.parse_string(testInput, parse_all=True)
|
||
print(testVal)
|
||
|
||
self.assertParseResultsEquals(testVal, expected_list=expected)
|
||
|
||
def testRepeaterRecursiveWhitespace(self):
|
||
"""test match_previous_expr with recursive whitespace"""
|
||
|
||
if ParserElement._packratEnabled or ParserElement._left_recursion_enabled:
|
||
print("skipping this test, not compatible with memoization")
|
||
return
|
||
|
||
COLON = pp.Suppress(":").set_name(":")
|
||
first = pp.Char(pp.nums)
|
||
second = pp.match_previous_expr(first)
|
||
expr = first + COLON + second
|
||
expr.leave_whitespace(recursive=True)
|
||
|
||
tests = [
|
||
("1:1", True),
|
||
("1: 1", False),
|
||
("1:2", False),
|
||
("1:a", False),
|
||
]
|
||
|
||
for tst, expected in tests:
|
||
if expected:
|
||
expected_int_list = [t.strip() for t in tst.split(":")]
|
||
self.assertParseAndCheckList(
|
||
expr, tst, expected_int_list,
|
||
msg=f"Failed recursive whitespace repeater test, expected pass: {expr=} {tst=}"
|
||
)
|
||
else:
|
||
with self.assertRaisesParseException(
|
||
msg=f"Failed recursive whitespace repeater test, expected fail: {expr=} {tst=}"
|
||
):
|
||
expr.parse_string(tst)
|
||
|
||
|
||
def testRepeaterRecursiveFalse(self):
|
||
"""test match_previous_expr with recursive=False"""
|
||
|
||
if ParserElement._packratEnabled or ParserElement._left_recursion_enabled:
|
||
print("skipping this test, not compatible with memoization")
|
||
return
|
||
|
||
COLON = pp.Suppress(":").set_name(":")
|
||
first = pp.Char(pp.nums)
|
||
second = pp.match_previous_expr(first)
|
||
expr = first + COLON + second
|
||
expr.leave_whitespace(recursive=False)
|
||
|
||
tests = [
|
||
("1:1", True),
|
||
("1: 1", True),
|
||
("1:2", False),
|
||
]
|
||
|
||
for tst, expected in tests:
|
||
if expected:
|
||
expected_int_list = [t.strip() for t in tst.split(":")]
|
||
self.assertParseAndCheckList(
|
||
expr, tst, expected_int_list,
|
||
msg=f"Failed recursive=False repeater test, expected pass: {expr=} {tst=}"
|
||
)
|
||
else:
|
||
with self.assertRaisesParseException(
|
||
msg=f"Failed recursive=False repeater test, expected fail: {expr=} {tst=}"
|
||
):
|
||
expr.parse_string(tst)
|
||
|
||
def testRepeaterPreservesParseAction(self):
|
||
"""test match_previous_expr preserves existing parse actions"""
|
||
|
||
if ParserElement._packratEnabled or ParserElement._left_recursion_enabled:
|
||
print("skipping this test, not compatible with memoization")
|
||
return
|
||
|
||
COLON = pp.Suppress(":").set_name(":")
|
||
first = ppc.integer
|
||
second = pp.match_previous_expr(first)
|
||
expr = first + COLON + second
|
||
expr.leave_whitespace(recursive=True)
|
||
|
||
tests = [
|
||
("1:1", True),
|
||
("1: 1", False),
|
||
("1:2", False),
|
||
]
|
||
|
||
for tst, expected in tests:
|
||
if expected:
|
||
expected_int_list = [int(t) for t in tst.split(":")]
|
||
self.assertParseAndCheckList(
|
||
expr, tst, expected_int_list,
|
||
msg=f"Failed parse action preservation repeater test, expected pass: {expr=} {tst=}"
|
||
)
|
||
else:
|
||
with self.assertRaisesParseException(
|
||
msg=f"Failed parse action preservation repeater test, expected fail: {expr=} {tst=}"
|
||
):
|
||
expr.parse_string(tst)
|
||
|
||
def testRepeaterNested(self):
|
||
"""test match_previous_expr with nested and repeated matches"""
|
||
|
||
if ParserElement._packratEnabled or ParserElement._left_recursion_enabled:
|
||
print("skipping this test, not compatible with memoization")
|
||
return
|
||
|
||
LBRACE, RBRACE, SLASH = map(pp.Suppress, "{}/")
|
||
tag_name = pp.Word(pp.alphanums + "_")
|
||
open_tag = LBRACE + tag_name + RBRACE
|
||
close_tag = LBRACE + SLASH + pp.match_previous_expr(tag_name) + RBRACE
|
||
|
||
content = pp.Forward()
|
||
content <<= (open_tag + pp.Group(content) + close_tag) | pp.Word(pp.alphas)
|
||
|
||
tests = [
|
||
("{a}xx{/a}", True),
|
||
("{a}{b}xx{/b}{/a}", True),
|
||
("{a}{b}{c}xx{/c}{/b}{/a}", True),
|
||
("{a}{b}xx{/a}{/b}", False),
|
||
("{a}{b}xx{/b}{/c}", False),
|
||
("{a}xx{/b}", False),
|
||
]
|
||
|
||
for tst, expected in tests:
|
||
if expected:
|
||
content.parse_string(tst, parse_all=True)
|
||
else:
|
||
with self.assertRaisesParseException(
|
||
msg=f"Failed nested repeater test, expected fail: {tst=}"
|
||
):
|
||
content.parse_string(tst, parse_all=True)
|
||
|
||
def testRepeaterReusedParser(self):
|
||
"""test match_previous_expr matches repeatedly, and after a failed parse"""
|
||
|
||
if ParserElement._packratEnabled or ParserElement._left_recursion_enabled:
|
||
print("skipping this test, not compatible with memoization")
|
||
return
|
||
|
||
first = pp.Word(pp.alphas)
|
||
second = pp.match_previous_expr(first)
|
||
expr = first + pp.Suppress(":") + second
|
||
|
||
# repeated matches within a single parse
|
||
self.assertParseAndCheckList(
|
||
pp.OneOrMore(expr), "a:a b:b c:c", ["a", "a", "b", "b", "c", "c"]
|
||
)
|
||
|
||
# a mismatch must not poison later parses of the same expression
|
||
tests = [("d:d", True), ("e:f", False), ("g:g", True)]
|
||
for tst, expected in tests:
|
||
if expected:
|
||
self.assertParseAndCheckList(
|
||
expr,
|
||
tst,
|
||
tst.split(":"),
|
||
msg=f"Failed reused repeater test, expected pass: {tst=}",
|
||
)
|
||
else:
|
||
with self.assertRaisesParseException(
|
||
msg=f"Failed reused repeater test, expected fail: {tst=}"
|
||
):
|
||
expr.parse_string(tst, parse_all=True)
|
||
|
||
def testSetNameToStrAndNone(self):
|
||
wd = pp.Word(pp.alphas)
|
||
with self.subTest():
|
||
self.assertEqual("W:(A-Za-z)", wd.name)
|
||
|
||
with self.subTest():
|
||
wd.set_name("test_word")
|
||
self.assertEqual("test_word", wd.name)
|
||
|
||
with self.subTest():
|
||
wd.set_name(None)
|
||
self.assertEqual("W:(A-Za-z)", wd.name)
|
||
|
||
# same tests but using name property setter
|
||
with self.subTest():
|
||
wd.name = "test_word"
|
||
self.assertEqual("test_word", wd.name)
|
||
|
||
with self.subTest():
|
||
wd.name = None
|
||
self.assertEqual("W:(A-Za-z)", wd.name)
|
||
|
||
def testCombineSetName(self):
|
||
ab = pp.Combine(
|
||
pp.Literal("a").set_name("AAA") | pp.Literal("b").set_name("BBB")
|
||
).set_name("AB")
|
||
self.assertEqual("AB", ab.name)
|
||
self.assertEqual("AB", str(ab))
|
||
with self.assertRaisesParseException(expected_msg="Expected AB"):
|
||
ab.parse_string("C")
|
||
|
||
def testHTMLEntities(self):
|
||
html_source = dedent(
|
||
"""\
|
||
This & that
|
||
2 > 1
|
||
0 < 1
|
||
Don't get excited!
|
||
I said "Don't get excited!"
|
||
Copyright © 2021
|
||
Dot ⟶ ˙
|
||
"""
|
||
)
|
||
transformer = pp.common_html_entity().add_parse_action(pp.replace_html_entity)
|
||
transformed = transformer.transform_string(html_source)
|
||
print(transformed)
|
||
|
||
expected = dedent(
|
||
"""\
|
||
This & that
|
||
2 > 1
|
||
0 < 1
|
||
Don't get excited!
|
||
I said "Don't get excited!"
|
||
Copyright © 2021
|
||
Dot ⟶ ˙
|
||
"""
|
||
)
|
||
self.assertEqual(expected, transformed)
|
||
|
||
def testInfixNotationBasicArithEval(self):
|
||
import ast
|
||
|
||
integer = pp.Word(pp.nums).set_parse_action(lambda t: int(t[0]))
|
||
variable = pp.Word(pp.alphas, exact=1)
|
||
operand = integer | variable
|
||
|
||
expop = pp.Literal("^")
|
||
signop = pp.one_of("+ -")
|
||
multop = pp.one_of("* /")
|
||
plusop = pp.one_of("+ -")
|
||
factop = pp.Literal("!")
|
||
|
||
# fmt: off
|
||
expr = pp.infix_notation(
|
||
operand,
|
||
[
|
||
(factop, 1, pp.OpAssoc.LEFT),
|
||
(expop, 2, pp.OpAssoc.RIGHT),
|
||
(signop, 1, pp.OpAssoc.RIGHT),
|
||
(multop, 2, pp.OpAssoc.LEFT),
|
||
(plusop, 2, pp.OpAssoc.LEFT),
|
||
],
|
||
)
|
||
# fmt: on
|
||
|
||
test = [
|
||
"9 + 2 + 3",
|
||
"9 + 2 * 3",
|
||
"(9 + 2) * 3",
|
||
"(9 + -2) * 3",
|
||
"(9 + --2) * 3",
|
||
"(9 + -2) * 3^2^2",
|
||
"(9! + -2) * 3^2^2",
|
||
"M*X + B",
|
||
"M*(X + B)",
|
||
"1+2*-3^4*5+-+-6",
|
||
"3!!",
|
||
]
|
||
expected = """[[9, '+', 2, '+', 3]]
|
||
[[9, '+', [2, '*', 3]]]
|
||
[[[9, '+', 2], '*', 3]]
|
||
[[[9, '+', ['-', 2]], '*', 3]]
|
||
[[[9, '+', ['-', ['-', 2]]], '*', 3]]
|
||
[[[9, '+', ['-', 2]], '*', [3, '^', [2, '^', 2]]]]
|
||
[[[[9, '!'], '+', ['-', 2]], '*', [3, '^', [2, '^', 2]]]]
|
||
[[['M', '*', 'X'], '+', 'B']]
|
||
[['M', '*', ['X', '+', 'B']]]
|
||
[[1, '+', [2, '*', ['-', [3, '^', 4]], '*', 5], '+', ['-', ['+', ['-', 6]]]]]
|
||
[[3, '!', '!']]""".split(
|
||
"\n"
|
||
)
|
||
expected = [ast.literal_eval(x.strip()) for x in expected]
|
||
for test_str, exp_list in zip(test, expected):
|
||
self.assertParseAndCheckList(expr, test_str, exp_list, verbose=True)
|
||
|
||
def testInfixNotationEvalBoolExprUsingAstClasses(self):
|
||
boolVars = {"True": True, "False": False}
|
||
|
||
class BoolOperand:
|
||
reprsymbol = ""
|
||
|
||
def __init__(self, t):
|
||
self.args = t[0][0::2]
|
||
|
||
def __str__(self):
|
||
sep = f" {self.reprsymbol} "
|
||
return f"({sep.join(map(str, self.args))})"
|
||
|
||
class BoolAnd(BoolOperand):
|
||
reprsymbol = "&"
|
||
|
||
def __bool__(self):
|
||
for a in self.args:
|
||
if isinstance(a, str):
|
||
v = boolVars[a]
|
||
else:
|
||
v = bool(a)
|
||
if not v:
|
||
return False
|
||
return True
|
||
|
||
class BoolOr(BoolOperand):
|
||
reprsymbol = "|"
|
||
|
||
def __bool__(self):
|
||
for a in self.args:
|
||
if isinstance(a, str):
|
||
v = boolVars[a]
|
||
else:
|
||
v = bool(a)
|
||
if v:
|
||
return True
|
||
return False
|
||
|
||
class BoolNot:
|
||
def __init__(self, t):
|
||
self.arg = t[0][1]
|
||
|
||
def __str__(self):
|
||
return f"~{self.arg}"
|
||
|
||
def __bool__(self):
|
||
if isinstance(self.arg, str):
|
||
v = boolVars[self.arg]
|
||
else:
|
||
v = bool(self.arg)
|
||
return not v
|
||
|
||
boolOperand = pp.Word(pp.alphas, max=1, as_keyword=True) | pp.one_of(
|
||
"True False"
|
||
)
|
||
# fmt: off
|
||
boolExpr = pp.infix_notation(
|
||
boolOperand,
|
||
[
|
||
("not", 1, pp.OpAssoc.RIGHT, BoolNot),
|
||
("and", 2, pp.OpAssoc.LEFT, BoolAnd),
|
||
("or", 2, pp.OpAssoc.LEFT, BoolOr),
|
||
],
|
||
)
|
||
# fmt: on
|
||
test = [
|
||
"p and not q",
|
||
"not not p",
|
||
"not(p and q)",
|
||
"q or not p and r",
|
||
"q or not p or not r",
|
||
"q or not (p and r)",
|
||
"p or q or r",
|
||
"p or q or r and False",
|
||
"(p or q or r) and False",
|
||
]
|
||
|
||
boolVars["p"] = True
|
||
boolVars["q"] = False
|
||
boolVars["r"] = True
|
||
print("p =", boolVars["p"])
|
||
print("q =", boolVars["q"])
|
||
print("r =", boolVars["r"])
|
||
print()
|
||
for t in test:
|
||
res = boolExpr.parse_string(t, parse_all=True)
|
||
print(t, "\n", res[0], "=", bool(res[0]), "\n")
|
||
expected = eval(t, {}, boolVars)
|
||
self.assertEqual(expected, bool(res[0]), f"failed boolean eval test {t}")
|
||
|
||
def testInfixNotationMinimalParseActionCalls(self):
|
||
count = 0
|
||
|
||
def evaluate_int(t):
|
||
nonlocal count
|
||
value = int(t[0])
|
||
print("evaluate_int", value)
|
||
count += 1
|
||
return value
|
||
|
||
integer = pp.Word(pp.nums).set_parse_action(evaluate_int)
|
||
variable = pp.Word(pp.alphas, exact=1)
|
||
operand = integer | variable
|
||
|
||
expop = pp.Literal("^")
|
||
signop = pp.one_of("+ -")
|
||
multop = pp.one_of("* /")
|
||
plusop = pp.one_of("+ -")
|
||
factop = pp.Literal("!")
|
||
|
||
# fmt: off
|
||
expr = pp.infix_notation(
|
||
operand,
|
||
[
|
||
(factop, 1, pp.OpAssoc.LEFT),
|
||
(expop, 2, pp.OpAssoc.LEFT),
|
||
(signop, 1, pp.OpAssoc.RIGHT),
|
||
(multop, 2, pp.OpAssoc.LEFT),
|
||
(plusop, 2, pp.OpAssoc.LEFT),
|
||
],
|
||
)
|
||
# fmt: on
|
||
|
||
test = ["9"]
|
||
for t in test:
|
||
count = 0
|
||
print(f"{t!r} => {expr.parse_string(t, parse_all=True)} (count={count})")
|
||
self.assertEqual(1, count, "count evaluated too many times!")
|
||
|
||
def testInfixNotationWithParseActions(self):
|
||
word = pp.Word(pp.alphas)
|
||
|
||
def supLiteral(s):
|
||
"""Returns the suppressed literal s"""
|
||
return pp.Literal(s).suppress()
|
||
|
||
def booleanExpr(atom):
|
||
ops = [
|
||
(supLiteral("!"), 1, pp.OpAssoc.RIGHT, lambda s, l, t: ["!", t[0][0]]),
|
||
(pp.one_of("= !="), 2, pp.OpAssoc.LEFT),
|
||
(supLiteral("&"), 2, pp.OpAssoc.LEFT, lambda s, l, t: ["&", t[0]]),
|
||
(supLiteral("|"), 2, pp.OpAssoc.LEFT, lambda s, l, t: ["|", t[0]]),
|
||
]
|
||
return pp.infix_notation(atom, ops)
|
||
|
||
f = booleanExpr(word) + pp.StringEnd()
|
||
|
||
tests = [
|
||
("bar = foo", [["bar", "=", "foo"]]),
|
||
(
|
||
"bar = foo & baz = fee",
|
||
["&", [["bar", "=", "foo"], ["baz", "=", "fee"]]],
|
||
),
|
||
]
|
||
for test, expected in tests:
|
||
print(test)
|
||
results = f.parse_string(test, parse_all=True)
|
||
print(results)
|
||
self.assertParseResultsEquals(results, expected_list=expected)
|
||
print()
|
||
|
||
def testInfixNotationGrammarTest5(self):
|
||
expop = pp.Literal("**")
|
||
signop = pp.one_of("+ -")
|
||
multop = pp.one_of("* /")
|
||
plusop = pp.one_of("+ -")
|
||
|
||
class ExprNode:
|
||
def __init__(self, tokens):
|
||
self.tokens = tokens[0]
|
||
|
||
def eval(self):
|
||
return None
|
||
|
||
class NumberNode(ExprNode):
|
||
def eval(self):
|
||
return self.tokens
|
||
|
||
class SignOp(ExprNode):
|
||
def eval(self):
|
||
mult = {"+": 1, "-": -1}[self.tokens[0]]
|
||
return mult * self.tokens[1].eval()
|
||
|
||
class BinOp(ExprNode):
|
||
opn_map = {}
|
||
|
||
def eval(self):
|
||
ret = self.tokens[0].eval()
|
||
for op, operand in zip(self.tokens[1::2], self.tokens[2::2]):
|
||
ret = self.opn_map[op](ret, operand.eval())
|
||
return ret
|
||
|
||
class ExpOp(BinOp):
|
||
opn_map = {"**": lambda a, b: b**a}
|
||
|
||
class MultOp(BinOp):
|
||
import operator
|
||
|
||
opn_map = {"*": operator.mul, "/": operator.truediv}
|
||
|
||
class AddOp(BinOp):
|
||
import operator
|
||
|
||
opn_map = {"+": operator.add, "-": operator.sub}
|
||
|
||
operand = ppc.number().set_parse_action(NumberNode)
|
||
# fmt: off
|
||
expr = pp.infix_notation(
|
||
operand,
|
||
[
|
||
(expop, 2, pp.OpAssoc.LEFT, (lambda pr: [pr[0][::-1]], ExpOp)),
|
||
(signop, 1, pp.OpAssoc.RIGHT, SignOp),
|
||
(multop, 2, pp.OpAssoc.LEFT, MultOp),
|
||
(plusop, 2, pp.OpAssoc.LEFT, AddOp),
|
||
],
|
||
)
|
||
# fmt: on
|
||
|
||
tests = """\
|
||
2+7
|
||
2**3
|
||
2**3**2
|
||
3**9
|
||
3**3**2
|
||
"""
|
||
|
||
for t in tests.splitlines():
|
||
t = t.strip()
|
||
if not t:
|
||
continue
|
||
|
||
parsed = expr.parse_string(t, parse_all=True)
|
||
eval_value = parsed[0].eval()
|
||
self.assertEqual(
|
||
eval(t),
|
||
eval_value,
|
||
f"Error evaluating {t!r}, expected {eval(t)!r}, got {eval_value!r}",
|
||
)
|
||
|
||
def testInfixNotationExceptions(self):
|
||
num = pp.Word(pp.nums)
|
||
|
||
# fmt: off
|
||
|
||
# arity 3 with None opExpr - should raise ValueError
|
||
with self.assertRaises(ValueError):
|
||
expr = pp.infix_notation(
|
||
num,
|
||
[
|
||
(None, 3, pp.OpAssoc.LEFT),
|
||
]
|
||
)
|
||
|
||
# arity 3 with invalid tuple - should raise ValueError
|
||
with self.assertRaises(ValueError):
|
||
expr = pp.infix_notation(
|
||
num,
|
||
[
|
||
(("+", "-", "*"), 3, pp.OpAssoc.LEFT),
|
||
]
|
||
)
|
||
|
||
# left arity > 3 - should raise ValueError
|
||
with self.assertRaises(ValueError):
|
||
expr = pp.infix_notation(
|
||
num,
|
||
[
|
||
("*", 4, pp.OpAssoc.LEFT),
|
||
]
|
||
)
|
||
|
||
# right arity > 3 - should raise ValueError
|
||
with self.assertRaises(ValueError):
|
||
expr = pp.infix_notation(
|
||
num,
|
||
[
|
||
("*", 4, pp.OpAssoc.RIGHT),
|
||
]
|
||
)
|
||
|
||
# assoc not from OpAssoc - should raise ValueError
|
||
with self.assertRaises(ValueError):
|
||
expr = pp.infix_notation(
|
||
num,
|
||
[
|
||
("*", 2, "LEFT"),
|
||
]
|
||
)
|
||
# fmt: on
|
||
|
||
def testInfixNotationWithNonOperators(self):
|
||
# left arity 2 with None expr
|
||
# right arity 2 with None expr
|
||
num = pp.Word(pp.nums).add_parse_action(pp.token_map(int))
|
||
ident = ppc.identifier()
|
||
|
||
# fmt: off
|
||
for assoc in (pp.OpAssoc.LEFT, pp.OpAssoc.RIGHT):
|
||
expr = pp.infix_notation(
|
||
num | ident,
|
||
[
|
||
(None, 2, assoc),
|
||
("+", 2, pp.OpAssoc.LEFT),
|
||
]
|
||
)
|
||
self.assertParseAndCheckList(expr, "3x+2", [[[3, "x"], "+", 2]])
|
||
# fmt: on
|
||
|
||
def testInfixNotationTernaryOperator(self):
|
||
# left arity 3
|
||
# right arity 3
|
||
num = pp.Word(pp.nums).add_parse_action(pp.token_map(int))
|
||
|
||
# fmt: off
|
||
for assoc in (pp.OpAssoc.LEFT, pp.OpAssoc.RIGHT):
|
||
expr = pp.infix_notation(
|
||
num,
|
||
[
|
||
("+", 2, pp.OpAssoc.LEFT),
|
||
(("?", ":"), 3, assoc),
|
||
]
|
||
)
|
||
self.assertParseAndCheckList(
|
||
expr, "3 + 2? 12: 13", [[[3, "+", 2], "?", 12, ":", 13]]
|
||
)
|
||
# fmt: on
|
||
|
||
def testInfixNotationWithAlternateParenSymbols(self):
|
||
num = pp.Word(pp.nums).add_parse_action(pp.token_map(int))
|
||
|
||
# fmt: off
|
||
expr = pp.infix_notation(
|
||
num,
|
||
[
|
||
("+", 2, pp.OpAssoc.LEFT),
|
||
],
|
||
lpar="(",
|
||
rpar=")",
|
||
)
|
||
self.assertParseAndCheckList(
|
||
expr, "3 + (2 + 11)", [[3, '+', [2, '+', 11]]]
|
||
)
|
||
|
||
expr = pp.infix_notation(
|
||
num,
|
||
[
|
||
("+", 2, pp.OpAssoc.LEFT),
|
||
],
|
||
lpar="<",
|
||
rpar=">",
|
||
)
|
||
self.assertParseAndCheckList(
|
||
expr, "3 + <2 + 11>", [[3, '+', [2, '+', 11]]]
|
||
)
|
||
|
||
expr = pp.infix_notation(
|
||
num,
|
||
[
|
||
("+", 2, pp.OpAssoc.LEFT),
|
||
],
|
||
lpar=pp.Literal("<"),
|
||
rpar=pp.Literal(">"),
|
||
)
|
||
self.assertParseAndCheckList(
|
||
expr, "3 + <2 + 11>", [[3, '+', ['<', [2, '+', 11], '>']]]
|
||
)
|
||
|
||
expr = pp.infix_notation(
|
||
num,
|
||
[
|
||
("+", 2, pp.OpAssoc.LEFT),
|
||
],
|
||
lpar=pp.Literal("<<"),
|
||
rpar=pp.Literal(">>"),
|
||
)
|
||
self.assertParseAndCheckList(
|
||
expr, "3 + <<2 + 11>>", [[3, '+', ['<<', [2, '+', 11], '>>']]]
|
||
)
|
||
|
||
# fmt: on
|
||
|
||
def testParseResultsPickle(self):
|
||
import pickle
|
||
|
||
# test 1
|
||
body = pp.make_html_tags("BODY")[0]
|
||
result = body.parse_string(
|
||
"<BODY BGCOLOR='#00FFBB' FGCOLOR=black>", parse_all=True
|
||
)
|
||
print(result.dump())
|
||
|
||
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
|
||
print("Test pickle dump protocol", protocol)
|
||
try:
|
||
pickleString = pickle.dumps(result, protocol)
|
||
except Exception as e:
|
||
print("dumps exception:", e)
|
||
newresult = pp.ParseResults()
|
||
else:
|
||
newresult = pickle.loads(pickleString)
|
||
print(newresult.dump())
|
||
|
||
self.assertEqual(
|
||
result.dump(),
|
||
newresult.dump(),
|
||
f"Error pickling ParseResults object (protocol={protocol})",
|
||
)
|
||
|
||
# check all internal attributes
|
||
for attr in pp.ParseResults.__slots__:
|
||
# can't pickle/unpickle parent reference
|
||
if attr == "_parent":
|
||
continue
|
||
self.assertEqual(
|
||
getattr(result, attr),
|
||
getattr(newresult, attr),
|
||
f"Mismatch in ParseResults.{attr}"
|
||
)
|
||
|
||
|
||
def testParseResultsPickle2(self):
|
||
import pickle
|
||
|
||
word = pp.Word(pp.alphas + "'.")
|
||
salutation = pp.OneOrMore(word)
|
||
comma = pp.Literal(",")
|
||
greetee = pp.OneOrMore(word)
|
||
endpunc = pp.one_of("! ?")
|
||
greeting = (
|
||
salutation("greeting")
|
||
+ pp.Suppress(comma)
|
||
+ greetee("greetee")
|
||
+ endpunc("punc*")[1, ...]
|
||
)
|
||
|
||
string = "Good morning, Miss Crabtree!"
|
||
|
||
result = greeting.parse_string(string, parse_all=True)
|
||
self.assertParseResultsEquals(
|
||
result,
|
||
["Good", "morning", "Miss", "Crabtree", "!"],
|
||
{
|
||
"greeting": ["Good", "morning"],
|
||
"greetee": ["Miss", "Crabtree"],
|
||
"punc": ["!"],
|
||
},
|
||
)
|
||
print(result.dump())
|
||
|
||
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
|
||
print("Test pickle dump protocol", protocol)
|
||
try:
|
||
pickleString = pickle.dumps(result, protocol)
|
||
except Exception as e:
|
||
print("dumps exception:", e)
|
||
newresult = pp.ParseResults()
|
||
else:
|
||
newresult = pickle.loads(pickleString)
|
||
print(newresult.dump())
|
||
self.assertEqual(
|
||
newresult.dump(),
|
||
result.dump(),
|
||
f"failed to pickle/unpickle ParseResults: expected {result!r}, got {newresult!r}",
|
||
)
|
||
|
||
def testParseResultsPickle3(self):
|
||
import pickle
|
||
|
||
# result with aslist=False
|
||
res_not_as_list = pp.Word("ABC").parse_string("BABBAB", parse_all=True)
|
||
|
||
# result with aslist=True
|
||
res_as_list = pp.Group(pp.Word("ABC")).parse_string("BABBAB", parse_all=True)
|
||
|
||
# result with modal=True
|
||
res_modal = pp.Word("ABC")("name").parse_string("BABBAB", parse_all=True)
|
||
# self.assertTrue(res_modal._modal)
|
||
|
||
# result with modal=False
|
||
res_not_modal = pp.Word("ABC")("name*").parse_string("BABBAB", parse_all=True)
|
||
# self.assertFalse(res_not_modal._modal)
|
||
|
||
for result in (res_as_list, res_not_as_list, res_modal, res_not_modal):
|
||
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
|
||
print("Test pickle dump protocol", protocol)
|
||
try:
|
||
pickleString = pickle.dumps(result, protocol)
|
||
except Exception as e:
|
||
print("dumps exception:", e)
|
||
newresult = pp.ParseResults()
|
||
else:
|
||
newresult = pickle.loads(pickleString)
|
||
print(newresult.dump())
|
||
self.assertEqual(
|
||
newresult.dump(),
|
||
result.dump(),
|
||
f"failed to pickle/unpickle ParseResults: expected {result!r}, got {newresult!r}",
|
||
)
|
||
|
||
def testParseResultsInsertWithResultsNames(self):
|
||
test_string = "1 2 3 dice rolled first try"
|
||
|
||
wd = pp.Word(pp.alphas)
|
||
num = ppc.number
|
||
|
||
expr = (
|
||
pp.Group(num[1, ...])("nums")
|
||
+ wd("label")
|
||
+ pp.Group(wd[...])("additional")
|
||
)
|
||
|
||
result = expr.parse_string(test_string, parse_all=True)
|
||
print("Pre-insert")
|
||
print(result.dump())
|
||
|
||
result.insert(1, sum(result.nums))
|
||
|
||
print("\nPost-insert")
|
||
print(result.dump())
|
||
|
||
self.assertParseResultsEquals(
|
||
result,
|
||
expected_list=[[1, 2, 3], 6, "dice", ["rolled", "first", "try"]],
|
||
expected_dict={
|
||
"additional": ["rolled", "first", "try"],
|
||
"label": "dice",
|
||
"nums": [1, 2, 3],
|
||
},
|
||
)
|
||
|
||
def testParseResultsStringListUsingCombine(self):
|
||
test_string = "1 2 3 dice rolled first try"
|
||
|
||
wd = pp.Word(pp.alphas)
|
||
num = ppc.number
|
||
|
||
expr = pp.Combine(
|
||
pp.Group(num[1, ...])("nums")
|
||
+ wd("label")
|
||
+ pp.Group(wd[...])("additional"),
|
||
join_string="/",
|
||
adjacent=False,
|
||
)
|
||
self.assertEqual(
|
||
"123/dice/rolledfirsttry", expr.parse_string(test_string, parse_all=True)[0]
|
||
)
|
||
|
||
def testParseResultsAcceptingACollectionTypeValue(self):
|
||
# from Issue #276 - ParseResults parameterizes generic types if passed as the value of toklist parameter
|
||
# https://github.com/pyparsing/pyparsing/issues/276?notification_referrer_id=MDE4Ok5vdGlmaWNhdGlvblRocmVhZDE4MzU4NDYwNzI6MzgzODc1
|
||
#
|
||
# behavior of ParseResults code changed with Python 3.9
|
||
|
||
results_with_int = pp.ParseResults(toklist=int, name="type_", aslist=False)
|
||
self.assertEqual(int, results_with_int["type_"])
|
||
|
||
results_with_tuple = pp.ParseResults(toklist=tuple, name="type_", aslist=False)
|
||
self.assertEqual(tuple, results_with_tuple["type_"])
|
||
|
||
def testParseResultsReturningDunderAttribute(self):
|
||
# from Issue #208
|
||
parser = pp.Word(pp.alphas)("A")
|
||
result = parser.parse_string("abc", parse_all=True)
|
||
print(result.dump())
|
||
self.assertEqual("abc", result.A)
|
||
self.assertEqual("", result.B)
|
||
with self.assertRaises(AttributeError):
|
||
result.__xyz__
|
||
|
||
def testParseResultsNamedResultWithEmptyString(self):
|
||
# from Issue #470
|
||
|
||
# Check which values can be returned from a parse action
|
||
for test_value, expected_in_result_by_name in [
|
||
("x", True),
|
||
("", True),
|
||
(True, True),
|
||
(False, True),
|
||
(1, True),
|
||
(0, True),
|
||
(None, True),
|
||
(b"", True),
|
||
(b"a", True),
|
||
([], False),
|
||
((), False),
|
||
]:
|
||
msg = (
|
||
f"value = {test_value!r},"
|
||
f" expected X {'not ' if not expected_in_result_by_name else ''}in result"
|
||
)
|
||
with self.subTest(msg):
|
||
print(msg)
|
||
grammar = (
|
||
(pp.Suppress("a") + pp.ZeroOrMore("x"))
|
||
.add_parse_action(lambda p: test_value)
|
||
.set_results_name("X")
|
||
)
|
||
result = grammar.parse_string("a")
|
||
print(result.dump())
|
||
if expected_in_result_by_name:
|
||
self.assertIn(
|
||
"X",
|
||
result,
|
||
f"Expected X not found for parse action value {test_value!r}",
|
||
)
|
||
print(repr(result["X"]))
|
||
else:
|
||
self.assertNotIn(
|
||
"X",
|
||
result,
|
||
f"Unexpected X found for parse action value {test_value!r}",
|
||
)
|
||
with self.assertRaises(KeyError):
|
||
print(repr(result["X"]))
|
||
print()
|
||
|
||
# Do not add a parse result.
|
||
msg = "value = <no parse action defined>, expected X in result"
|
||
with self.subTest(msg):
|
||
print(msg)
|
||
grammar = (pp.Suppress("a") + pp.ZeroOrMore("x")).set_results_name("X")
|
||
result = grammar.parse_string("a")
|
||
print(result.dump())
|
||
self.assertIn("X", result, f"Expected X not found with no parse action")
|
||
print()
|
||
|
||
# Test by directly creating a ParseResults
|
||
print("Create empty string value directly")
|
||
result = pp.ParseResults("", name="X")
|
||
print(result.dump())
|
||
self.assertIn(
|
||
"X",
|
||
result,
|
||
"failed to construct ParseResults with named value using empty string",
|
||
)
|
||
print(repr(result["X"]))
|
||
print()
|
||
|
||
print("Create empty string value from a dict")
|
||
result = pp.ParseResults.from_dict({"X": ""})
|
||
print(result.dump())
|
||
self.assertIn(
|
||
"X",
|
||
result,
|
||
"failed to construct ParseResults with named value using from_dict",
|
||
)
|
||
print(repr(result["X"]))
|
||
|
||
def testMatchOnlyAtCol(self):
|
||
"""successfully use match_only_at_col helper function"""
|
||
|
||
expr = pp.Word(pp.nums)
|
||
expr.set_parse_action(pp.match_only_at_col(5))
|
||
largerExpr = pp.ZeroOrMore(pp.Word("A")) + expr + pp.ZeroOrMore(pp.Word("A"))
|
||
|
||
res = largerExpr.parse_string("A A 3 A", parse_all=True)
|
||
print(res.dump())
|
||
|
||
def testMatchOnlyAtColErr(self):
|
||
"""raise a ParseException in match_only_at_col with incorrect col"""
|
||
|
||
expr = pp.Word(pp.nums)
|
||
expr.set_parse_action(pp.match_only_at_col(1))
|
||
largerExpr = pp.ZeroOrMore(pp.Word("A")) + expr + pp.ZeroOrMore(pp.Word("A"))
|
||
|
||
with self.assertRaisesParseException():
|
||
largerExpr.parse_string("A A 3 A", parse_all=True)
|
||
|
||
def testParseResultsWithNamedTuple(self):
|
||
expr = pp.Literal("A")("Achar")
|
||
expr.set_parse_action(pp.replace_with(tuple(["A", "Z"])))
|
||
|
||
res = expr.parse_string("A", parse_all=True)
|
||
print(repr(res))
|
||
print(res.Achar)
|
||
self.assertParseResultsEquals(
|
||
res,
|
||
expected_dict={"Achar": ("A", "Z")},
|
||
msg=f"Failed accessing named results containing a tuple, got {res.Achar!r}",
|
||
)
|
||
|
||
def testParserElementAddOperatorWithOtherTypes(self):
|
||
"""test the overridden "+" operator with other data types"""
|
||
|
||
# ParserElement + str
|
||
with self.subTest():
|
||
expr = pp.Word(pp.alphas)("first") + pp.Word(pp.alphas)("second") + "suf"
|
||
result = expr.parse_string("spam eggs suf", parse_all=True)
|
||
print(result)
|
||
|
||
expected_l = ["spam", "eggs", "suf"]
|
||
self.assertParseResultsEquals(
|
||
result, expected_l, msg="issue with ParserElement + str"
|
||
)
|
||
|
||
# str + ParserElement
|
||
with self.subTest():
|
||
expr = "pre" + pp.Word(pp.alphas)("first") + pp.Word(pp.alphas)("second")
|
||
result = expr.parse_string("pre spam eggs", parse_all=True)
|
||
print(result)
|
||
|
||
expected_l = ["pre", "spam", "eggs"]
|
||
self.assertParseResultsEquals(
|
||
result, expected_l, msg="issue with str + ParserElement"
|
||
)
|
||
|
||
# ParserElement + int
|
||
with self.subTest():
|
||
expr = None
|
||
with self.assertRaises(TypeError, msg="failed to warn ParserElement + int"):
|
||
expr = pp.Word(pp.alphas)("first") + pp.Word(pp.alphas)("second") + 12
|
||
self.assertEqual(expr, None)
|
||
|
||
# int + ParserElement
|
||
with self.subTest():
|
||
expr = None
|
||
with self.assertRaises(TypeError, msg="failed to warn int + ParserElement"):
|
||
expr = 12 + pp.Word(pp.alphas)("first") + pp.Word(pp.alphas)("second")
|
||
self.assertEqual(expr, None)
|
||
|
||
def testParserElementSubOperatorWithOtherTypes(self):
|
||
"""test the overridden "-" operator with other data types"""
|
||
|
||
# ParserElement - str
|
||
with self.subTest():
|
||
expr = pp.Word(pp.alphas)("first") + pp.Word(pp.alphas)("second") - "suf"
|
||
result = expr.parse_string("spam eggs suf", parse_all=True)
|
||
print(result)
|
||
expected = ["spam", "eggs", "suf"]
|
||
self.assertParseResultsEquals(
|
||
result, expected, msg="issue with ParserElement - str"
|
||
)
|
||
|
||
# str - ParserElement
|
||
with self.subTest():
|
||
expr = "pre" - pp.Word(pp.alphas)("first") + pp.Word(pp.alphas)("second")
|
||
result = expr.parse_string("pre spam eggs", parse_all=True)
|
||
print(result)
|
||
expected = ["pre", "spam", "eggs"]
|
||
self.assertParseResultsEquals(
|
||
result, expected, msg="issue with str - ParserElement"
|
||
)
|
||
|
||
# ParserElement - int
|
||
with self.subTest():
|
||
expr = None
|
||
with self.assertRaises(TypeError, msg="failed to warn ParserElement - int"):
|
||
expr = pp.Word(pp.alphas)("first") + pp.Word(pp.alphas)("second") - 12
|
||
self.assertEqual(expr, None)
|
||
|
||
# int - ParserElement
|
||
with self.subTest():
|
||
expr = None
|
||
with self.assertRaises(TypeError, msg="failed to warn int - ParserElement"):
|
||
expr = 12 - pp.Word(pp.alphas)("first") + pp.Word(pp.alphas)("second")
|
||
self.assertEqual(expr, None)
|
||
|
||
def testParserElementMulOperatorWithTuples(self):
|
||
"""test ParserElement "*" with various tuples"""
|
||
|
||
# ParserElement * (None, n)
|
||
expr = pp.Word(pp.alphas)("first") + pp.Word(pp.nums)("second*") * (None, 3)
|
||
|
||
with self.subTest():
|
||
results1 = expr.parse_string("spam", parse_all=True)
|
||
print(results1.dump())
|
||
expected = ["spam"]
|
||
self.assertParseResultsEquals(
|
||
results1, expected, msg="issue with ParserElement * w/ optional matches"
|
||
)
|
||
|
||
with self.subTest():
|
||
results2 = expr.parse_string("spam 12 23 34", parse_all=True)
|
||
print(results2.dump())
|
||
expected = ["spam", "12", "23", "34"]
|
||
self.assertParseResultsEquals(
|
||
results2, expected, msg="issue with ParserElement * w/ optional matches"
|
||
)
|
||
|
||
# ParserElement * (1, 1)
|
||
with self.subTest():
|
||
expr = pp.Word(pp.alphas)("first") + pp.Word(pp.nums)("second*") * (1, 1)
|
||
results = expr.parse_string("spam 45", parse_all=True)
|
||
print(results.dump())
|
||
|
||
expected = ["spam", "45"]
|
||
self.assertParseResultsEquals(
|
||
results, expected, msg="issue with ParserElement * (1, 1)"
|
||
)
|
||
|
||
# ParserElement * (1, 1+n)
|
||
with self.subTest():
|
||
expr = pp.Word(pp.alphas)("first") + pp.Word(pp.nums)("second*") * (1, 3)
|
||
|
||
results1 = expr.parse_string("spam 100", parse_all=True)
|
||
print(results1.dump())
|
||
expected = ["spam", "100"]
|
||
self.assertParseResultsEquals(
|
||
results1, expected, msg="issue with ParserElement * (1, 1+n)"
|
||
)
|
||
|
||
with self.subTest():
|
||
results2 = expr.parse_string("spam 100 200 300", parse_all=True)
|
||
print(results2.dump())
|
||
expected = ["spam", "100", "200", "300"]
|
||
self.assertParseResultsEquals(
|
||
results2, expected, msg="issue with ParserElement * (1, 1+n)"
|
||
)
|
||
|
||
# ParserElement * (lesser, greater)
|
||
with self.subTest():
|
||
expr = pp.Word(pp.alphas)("first") + pp.Word(pp.nums)("second*") * (2, 3)
|
||
|
||
results1 = expr.parse_string("spam 1 2", parse_all=True)
|
||
print(results1.dump())
|
||
expected = ["spam", "1", "2"]
|
||
self.assertParseResultsEquals(
|
||
results1, expected, msg="issue with ParserElement * (lesser, greater)"
|
||
)
|
||
|
||
with self.subTest():
|
||
results2 = expr.parse_string("spam 1 2 3", parse_all=True)
|
||
print(results2.dump())
|
||
expected = ["spam", "1", "2", "3"]
|
||
self.assertParseResultsEquals(
|
||
results2, expected, msg="issue with ParserElement * (lesser, greater)"
|
||
)
|
||
|
||
# ParserElement * (greater, lesser)
|
||
with self.subTest():
|
||
with self.assertRaises(
|
||
ValueError, msg="ParserElement * (greater, lesser) should raise error"
|
||
):
|
||
expr = pp.Word(pp.alphas)("first") + pp.Word(pp.nums)("second") * (3, 2)
|
||
|
||
# ParserElement * (str, str)
|
||
with self.subTest():
|
||
with self.assertRaises(
|
||
TypeError, msg="ParserElement * (str, str) should raise error"
|
||
):
|
||
expr = pp.Word(pp.alphas)("first") + pp.Word(pp.nums)("second") * (
|
||
"2",
|
||
"3",
|
||
)
|
||
|
||
def testBoundedRepetitionLargeUpperBound(self):
|
||
"""issue #332 - expr[..., upper_bound] (and expr * (0, upper_bound))
|
||
with a large upper_bound must not raise RecursionError during
|
||
construction or parsing.
|
||
"""
|
||
# Lower the recursion limit so a deeply nested implementation would
|
||
# fail deterministically, instead of depending on the host's default.
|
||
with self.resetting_recursion_limit():
|
||
sys.setrecursionlimit(300)
|
||
# both the [...] shorthand and the explicit *(0, n) form
|
||
expr_shorthand = pp.Literal("A")[..., 1000]
|
||
expr_tuple = pp.Word(pp.nums) * (0, 1000)
|
||
|
||
# check for recursion during parsing
|
||
expr_shorthand.parse_string("A " * 1000)
|
||
|
||
# construction must succeed
|
||
self.assertIsNotNone(expr_shorthand)
|
||
self.assertIsNotNone(expr_tuple)
|
||
|
||
# parsing a small input must succeed and yield the matched tokens
|
||
test_string = "A A A A A"
|
||
expected_list = test_string.split()
|
||
self.assertParseAndCheckList(
|
||
expr_shorthand, test_string, expected_list
|
||
)
|
||
|
||
# the upper bound is still enforced (only 3 words allowed before num)
|
||
with self.assertRaisesParseException():
|
||
(pp.Word(pp.alphas)[..., 3] + pp.Word(pp.nums)).parse_string("a b c d 1")
|
||
|
||
def testBoundedRepetitionLargeUpperBoundEarlyExit(self):
|
||
"""issue #332 - the optional tail of ``expr[..., upper_bound]`` must
|
||
still *exit early* (like the original recursive implementation): once
|
||
the repeated expression can no longer match, parsing must stop instead
|
||
of attempting it for every remaining slot in the upper bound.
|
||
|
||
This guards against a regression where the tail was flattened into a
|
||
list of independent ``Optional`` expressions, which always tried to
|
||
match the repeated expression for all ``upper_bound`` slots.
|
||
"""
|
||
def count_fails(*args):
|
||
count_fails.fail_counter += 1
|
||
count_fails.fail_counter = 0
|
||
|
||
A_expr = pp.Literal("A").set_fail_action(count_fails)
|
||
expr = A_expr[..., 1000]
|
||
|
||
# input has a single match, so a correct early-exit implementation
|
||
# must fail the repeated expression only once -- not
|
||
# once per slot in the (large) upper bound.
|
||
expr.parse_string("A")
|
||
self.assertEqual(
|
||
count_fails.fail_counter,
|
||
1,
|
||
"bounded repetition did not exit early; it attempted the"
|
||
f" repeated expression {count_fails.fail_counter} times for a single match",
|
||
)
|
||
|
||
def testParserElementMulByZero(self):
|
||
alpwd = pp.Word(pp.alphas)
|
||
numwd = pp.Word(pp.nums)
|
||
|
||
test_string = "abd def ghi jkl"
|
||
|
||
with self.subTest():
|
||
parser = alpwd * 2 + numwd * 0 + alpwd * 2
|
||
self.assertParseAndCheckList(
|
||
parser, test_string, expected_list=test_string.split()
|
||
)
|
||
|
||
with self.subTest():
|
||
parser = alpwd * 2 + numwd * (0, 0) + alpwd * 2
|
||
self.assertParseAndCheckList(
|
||
parser, test_string, expected_list=test_string.split()
|
||
)
|
||
|
||
def testParserElementMulOperatorWithOtherTypes(self):
|
||
"""test the overridden "*" operator with other data types"""
|
||
|
||
# ParserElement * str
|
||
with self.subTest():
|
||
with self.assertRaises(
|
||
TypeError, msg="ParserElement * str should raise error"
|
||
):
|
||
expr = pp.Word(pp.alphas)("first") + pp.Word(pp.nums)("second") * "3"
|
||
|
||
# str * ParserElement
|
||
with self.subTest():
|
||
with self.assertRaises(
|
||
TypeError, msg="str * ParserElement should raise error"
|
||
):
|
||
expr = pp.Word(pp.alphas)("first") + "3" * pp.Word(pp.nums)("second")
|
||
|
||
# ParserElement * int
|
||
with self.subTest():
|
||
expr = pp.Word(pp.alphas)("first") + pp.Word(pp.nums)("second*") * 2
|
||
results = expr.parse_string("spam 11 22", parse_all=True)
|
||
|
||
print(results.dump())
|
||
expected = ["spam", "11", "22"]
|
||
self.assertParseResultsEquals(
|
||
results, expected, msg="issue with ParserElement * int"
|
||
)
|
||
|
||
# int * ParserElement
|
||
with self.subTest():
|
||
expr = pp.Word(pp.alphas)("first") + 2 * pp.Word(pp.nums)("second*")
|
||
results = expr.parse_string("spam 111 222", parse_all=True)
|
||
|
||
print(results.dump())
|
||
expected = ["spam", "111", "222"]
|
||
self.assertParseResultsEquals(
|
||
results, expected, msg="issue with int * ParserElement"
|
||
)
|
||
|
||
def testParserElementMatchFirstOperatorWithOtherTypes(self):
|
||
"""test the overridden "|" operator with other data types"""
|
||
|
||
# ParserElement | int
|
||
with self.subTest():
|
||
expr = None
|
||
with self.assertRaises(TypeError, msg="failed to warn ParserElement | int"):
|
||
expr = pp.Word(pp.alphas)("first") + (pp.Word(pp.alphas)("second") | 12)
|
||
self.assertEqual(expr, None)
|
||
|
||
# int | ParserElement
|
||
with self.subTest():
|
||
expr = None
|
||
with self.assertRaises(TypeError, msg="failed to warn int | ParserElement"):
|
||
expr = pp.Word(pp.alphas)("first") + (12 | pp.Word(pp.alphas)("second"))
|
||
self.assertEqual(expr, None)
|
||
|
||
def testParserElementMatchLongestWithOtherTypes(self):
|
||
"""test the overridden "^" operator with other data types"""
|
||
|
||
# ParserElement ^ str
|
||
with self.subTest():
|
||
expr = pp.Word(pp.alphas)("first") + (pp.Word(pp.nums)("second") ^ "eggs")
|
||
result = expr.parse_string("spam eggs", parse_all=True)
|
||
print(result)
|
||
|
||
expected = ["spam", "eggs"]
|
||
self.assertParseResultsEquals(
|
||
result, expected, msg="issue with ParserElement ^ str"
|
||
)
|
||
|
||
# str ^ ParserElement
|
||
with self.subTest():
|
||
expr = ("pre" ^ pp.Word("pr")("first")) + pp.Word(pp.alphas)("second")
|
||
result = expr.parse_string("pre eggs", parse_all=True)
|
||
print(result)
|
||
|
||
expected = ["pre", "eggs"]
|
||
self.assertParseResultsEquals(
|
||
result, expected, msg="issue with str ^ ParserElement"
|
||
)
|
||
|
||
# ParserElement ^ int
|
||
with self.subTest():
|
||
expr = None
|
||
with self.assertRaises(TypeError, msg="failed to warn ParserElement ^ int"):
|
||
expr = pp.Word(pp.alphas)("first") + (pp.Word(pp.alphas)("second") ^ 54)
|
||
self.assertEqual(expr, None)
|
||
|
||
# int ^ ParserElement
|
||
with self.subTest():
|
||
expr = None
|
||
with self.assertRaises(TypeError, msg="failed to warn int ^ ParserElement"):
|
||
expr = pp.Word(pp.alphas)("first") + (65 ^ pp.Word(pp.alphas)("second"))
|
||
self.assertEqual(expr, None)
|
||
|
||
def testParserElementEachOperatorWithOtherTypes(self):
|
||
"""test the overridden "&" operator with other data types"""
|
||
|
||
# ParserElement & str
|
||
with self.subTest():
|
||
expr = pp.Word(pp.alphas)("first") + (pp.Word(pp.alphas)("second") & "and")
|
||
with self.assertRaisesParseException(msg="issue with ParserElement & str"):
|
||
result = expr.parse_string("spam and eggs", parse_all=True)
|
||
|
||
# str & ParserElement
|
||
with self.subTest():
|
||
expr = pp.Word(pp.alphas)("first") + ("and" & pp.Word(pp.alphas)("second"))
|
||
result = expr.parse_string("spam and eggs", parse_all=True)
|
||
|
||
print(result.dump())
|
||
expected_l = ["spam", "and", "eggs"]
|
||
expected_d = {"first": "spam", "second": "eggs"}
|
||
self.assertParseResultsEquals(
|
||
result,
|
||
expected_list=expected_l,
|
||
expected_dict=expected_d,
|
||
msg="issue with str & ParserElement",
|
||
)
|
||
|
||
# ParserElement & int
|
||
with self.subTest():
|
||
expr = None
|
||
with self.assertRaises(TypeError, msg="failed to warn ParserElement & int"):
|
||
expr = pp.Word(pp.alphas)("first") + (pp.Word(pp.alphas) & 78)
|
||
self.assertEqual(expr, None)
|
||
|
||
# int & ParserElement
|
||
with self.subTest():
|
||
expr = None
|
||
with self.assertRaises(TypeError, msg="failed to warn int & ParserElement"):
|
||
expr = pp.Word(pp.alphas)("first") + (89 & pp.Word(pp.alphas))
|
||
self.assertEqual(expr, None)
|
||
|
||
def testLshiftOperatorWithOtherTypes(self):
|
||
# Forward << ParserElement
|
||
with self.subTest():
|
||
f = pp.Forward()
|
||
f << pp.Word(pp.alphas)[...]
|
||
test_string = "sljdf sldkjf Ljs"
|
||
result = f.parse_string(test_string)
|
||
print(result)
|
||
self.assertEqual(test_string.split(), result.as_list())
|
||
|
||
# Forward << str
|
||
with self.subTest():
|
||
f = pp.Forward()
|
||
f << "AAA"
|
||
test_string = "AAA"
|
||
result = f.parse_string(test_string)
|
||
print(result)
|
||
self.assertEqual(test_string.split(), result.as_list())
|
||
|
||
# Forward << int
|
||
with self.subTest():
|
||
f = pp.Forward()
|
||
with self.assertRaises(TypeError, msg="failed to warn int & ParserElement"):
|
||
f << 12
|
||
|
||
def testParserElementPassedThreeArgsToMultiplierShorthand(self):
|
||
"""test the ParserElement form expr[m,n,o]"""
|
||
|
||
with self.assertRaises(
|
||
TypeError, msg="failed to warn three index arguments to expr[m, n, o]"
|
||
):
|
||
expr = pp.Word(pp.alphas)[2, 3, 4]
|
||
|
||
def testParserElementPassedStrToMultiplierShorthand(self):
|
||
"""test the ParserElement form expr[str]"""
|
||
|
||
with self.assertRaises(
|
||
TypeError, msg="failed to raise expected error using string multiplier"
|
||
):
|
||
expr2 = pp.Word(pp.alphas)["2"]
|
||
|
||
def testParseResultsNewEdgeCases(self):
|
||
"""test less common paths of ParseResults.__new__()"""
|
||
|
||
parser = pp.Word(pp.alphas)[...]
|
||
result = parser.parse_string("sldkjf sldkjf", parse_all=True)
|
||
|
||
# hasattr uses __getattr__, which for ParseResults will return "" if the
|
||
# results name is not defined. So hasattr() won't work with ParseResults.
|
||
# Have to use __contains__ instead to test for existence.
|
||
# self.assertFalse(hasattr(result, "A"))
|
||
self.assertFalse("A" in result)
|
||
|
||
# create new ParseResults w/ None
|
||
result1 = pp.ParseResults(None)
|
||
print(result1.dump())
|
||
self.assertParseResultsEquals(
|
||
result1, [], msg="ParseResults(None) should return empty ParseResults"
|
||
)
|
||
|
||
# create new ParseResults w/ integer name
|
||
result2 = pp.ParseResults(name=12)
|
||
print(result2.dump())
|
||
self.assertEqual(
|
||
"12",
|
||
result2.get_name(),
|
||
"ParseResults int name should be accepted and converted to str",
|
||
)
|
||
|
||
# create new ParseResults w/ generator type
|
||
gen = (a for a in range(1, 6))
|
||
result3 = pp.ParseResults(gen)
|
||
print(result3.dump())
|
||
expected3 = [1, 2, 3, 4, 5]
|
||
self.assertParseResultsEquals(
|
||
result3, expected3, msg="issue initializing ParseResults w/ gen type"
|
||
)
|
||
|
||
def testParseResultsReversed(self):
|
||
"""test simple case of reversed(ParseResults)"""
|
||
|
||
tst = "1 2 3 4 5"
|
||
expr = pp.OneOrMore(pp.Word(pp.nums))
|
||
result = expr.parse_string(tst, parse_all=True)
|
||
|
||
reversed_list = [ii for ii in reversed(result)]
|
||
print(reversed_list)
|
||
expected = ["5", "4", "3", "2", "1"]
|
||
self.assertEqual(
|
||
expected, reversed_list, msg="issue calling reversed(ParseResults)"
|
||
)
|
||
|
||
def testParseResultsValues(self):
|
||
"""test simple case of ParseResults.values()"""
|
||
|
||
expr = pp.Word(pp.alphas)("first") + pp.Word(pp.alphas)("second")
|
||
result = expr.parse_string("spam eggs", parse_all=True)
|
||
|
||
values_set = set(result.values())
|
||
print(values_set)
|
||
expected = {"spam", "eggs"}
|
||
self.assertEqual(
|
||
expected, values_set, msg="issue calling ParseResults.values()"
|
||
)
|
||
|
||
def testParseResultsAppend(self):
|
||
"""test simple case of ParseResults.append()"""
|
||
|
||
# use a parse action to compute the sum of the parsed integers, and add it to the end
|
||
def append_sum(tokens):
|
||
tokens.append(sum(map(int, tokens)))
|
||
|
||
expr = pp.OneOrMore(pp.Word(pp.nums)).add_parse_action(append_sum)
|
||
result = expr.parse_string("0 123 321", parse_all=True)
|
||
|
||
expected = ["0", "123", "321", 444]
|
||
print(result.dump())
|
||
self.assertParseResultsEquals(
|
||
result, expected, msg="issue with ParseResults.append()"
|
||
)
|
||
|
||
def testParseResultsArithmeticContract(self):
|
||
"""test ParseResults.__add__ and __radd__ arithmetic contract"""
|
||
|
||
import operator
|
||
|
||
pr = pp.ParseResults(["a", "b"])
|
||
|
||
# ParseResults + ParseResults (normal case)
|
||
pr_sum = pr + pp.ParseResults(["c"])
|
||
self.assertEqual(["a", "b", "c"], list(pr_sum))
|
||
|
||
# ParseResults + non-ParseResults -> TypeError (was AttributeError)
|
||
with self.assertRaises(
|
||
TypeError,
|
||
msg="ParseResults + int should raise TypeError, not AttributeError",
|
||
):
|
||
pr + 1
|
||
|
||
with self.assertRaises(
|
||
TypeError,
|
||
msg="ParseResults + str should raise TypeError",
|
||
):
|
||
pr + "extra"
|
||
|
||
# non-zero int + ParseResults -> TypeError (was RecursionError)
|
||
with self.assertRaises(
|
||
TypeError,
|
||
msg="non-zero int + ParseResults should raise TypeError, not RecursionError",
|
||
):
|
||
1 + pr
|
||
|
||
with self.assertRaises(
|
||
TypeError,
|
||
msg="float + ParseResults should raise TypeError",
|
||
):
|
||
1.5 + pr
|
||
|
||
with self.assertRaises(
|
||
TypeError,
|
||
msg="str + ParseResults should raise TypeError",
|
||
):
|
||
"x" + pr
|
||
|
||
# 0 + ParseResults works (supports sum() builtin)
|
||
zero_sum = 0 + pr
|
||
self.assertEqual(["a", "b"], list(zero_sum))
|
||
|
||
# sum() of ParseResults objects works
|
||
total = sum(
|
||
[
|
||
pp.ParseResults(["x"]),
|
||
pp.ParseResults(["y"]),
|
||
pp.ParseResults(["z"]),
|
||
]
|
||
)
|
||
self.assertEqual(["x", "y", "z"], list(total))
|
||
|
||
# reduce(operator.add, ...) also works
|
||
import functools
|
||
|
||
reduced = functools.reduce(
|
||
operator.add,
|
||
[pp.ParseResults(["m"]), pp.ParseResults(["n"])],
|
||
)
|
||
self.assertEqual(["m", "n"], list(reduced))
|
||
|
||
# ParseResults += non-ParseResults -> TypeError (was AttributeError),
|
||
# mirroring the __add__ contract above.
|
||
pr_iadd = pp.ParseResults(["a", "b"])
|
||
with self.assertRaises(
|
||
TypeError,
|
||
msg="ParseResults += int should raise TypeError, not AttributeError",
|
||
):
|
||
pr_iadd += 1
|
||
with self.assertRaises(
|
||
TypeError,
|
||
msg="ParseResults += list should raise TypeError, not AttributeError",
|
||
):
|
||
pr_iadd += ["extra"]
|
||
|
||
# ParseResults += ParseResults still works
|
||
pr_iadd += pp.ParseResults(["c"])
|
||
self.assertEqual(["a", "b", "c"], list(pr_iadd))
|
||
|
||
def testParseResultsClear(self):
|
||
"""test simple case of ParseResults.clear()"""
|
||
|
||
tst = "spam eggs"
|
||
expr = pp.Word(pp.alphas)("first") + pp.Word(pp.alphas)("second")
|
||
result = expr.parse_string(tst, parse_all=True)
|
||
|
||
print(result.dump())
|
||
self.assertParseResultsEquals(
|
||
result, ["spam", "eggs"], msg="issue with ParseResults before clear()"
|
||
)
|
||
|
||
result.clear()
|
||
|
||
print(result.dump())
|
||
self.assertParseResultsEquals(
|
||
result,
|
||
expected_list=[],
|
||
expected_dict={},
|
||
msg="issue with ParseResults.clear()",
|
||
)
|
||
|
||
def testParseResultsExtendWithString(self):
|
||
"""test ParseResults.extend() with input of type str"""
|
||
|
||
# use a parse action to append the reverse of the matched strings to make a palindrome
|
||
def make_palindrome(tokens):
|
||
tokens.extend(reversed([t[::-1] for t in tokens]))
|
||
|
||
tst = "abc def ghi"
|
||
expr = pp.OneOrMore(pp.Word(pp.alphas))
|
||
result = expr.add_parse_action(make_palindrome).parse_string(
|
||
tst, parse_all=True
|
||
)
|
||
print(result.dump())
|
||
|
||
expected = ["abc", "def", "ghi", "ihg", "fed", "cba"]
|
||
self.assertParseResultsEquals(
|
||
result, expected, msg="issue with ParseResults.extend(str)"
|
||
)
|
||
|
||
def testParseResultsExtendWithParseResults(self):
|
||
"""test ParseResults.extend() with input of type ParseResults"""
|
||
|
||
expr = pp.OneOrMore(pp.Word(pp.alphas))
|
||
result1 = expr.parse_string("spam eggs", parse_all=True)
|
||
result2 = expr.parse_string("foo bar", parse_all=True)
|
||
|
||
result1.extend(result2)
|
||
print(result1.dump())
|
||
expected = ["spam", "eggs", "foo", "bar"]
|
||
self.assertParseResultsEquals(
|
||
result1, expected, msg="issue with ParseResults.extend(ParseResults)"
|
||
)
|
||
|
||
def testQuotedStringLoc(self):
|
||
expr = pp.QuotedString("'")
|
||
expr.add_parse_action(lambda t: t[0].upper())
|
||
|
||
test_string = "Using 'quotes' for 'sarcasm' or 'emphasis' is not good 'style'."
|
||
transformed = expr.transform_string(test_string)
|
||
print(test_string)
|
||
print(transformed)
|
||
expected = re.sub(r"'([^']+)'", lambda match: match[1].upper(), test_string)
|
||
self.assertEqual(expected, transformed)
|
||
|
||
def testParseResultsWithNestedNames(self):
|
||
from pyparsing import (
|
||
Dict,
|
||
Literal,
|
||
Group,
|
||
Optional,
|
||
Regex,
|
||
QuotedString,
|
||
one_of,
|
||
Or,
|
||
CaselessKeyword,
|
||
ZeroOrMore,
|
||
)
|
||
|
||
RELATION_SYMBOLS = "= > < >= <= <> =="
|
||
|
||
def _set_info(string, location, tokens):
|
||
for t in tokens:
|
||
try:
|
||
t["_info_"] = (string, location)
|
||
except TypeError:
|
||
pass
|
||
tokens["_info_"] = (string, location)
|
||
|
||
def keywords(name):
|
||
words = "any all within encloses adj".split()
|
||
return Or(map(CaselessKeyword, words))
|
||
|
||
charString1 = Group(Regex(r'[^()=<>"/\s]+'))("identifier")
|
||
charString1.add_parse_action(_set_info)
|
||
charString2 = Group(QuotedString('"', "\\"))("quoted")
|
||
charString2.add_parse_action(_set_info)
|
||
|
||
term = Group(charString1 | charString2)
|
||
modifier_key = charString1
|
||
|
||
# relations
|
||
comparitor_symbol = one_of(RELATION_SYMBOLS)
|
||
named_comparitors = keywords("comparitors")
|
||
comparitor = Group(comparitor_symbol | named_comparitors)("comparitor")
|
||
comparitor.add_parse_action(_set_info)
|
||
|
||
def modifier_list1(key):
|
||
modifier = Dict(
|
||
Literal("/")
|
||
+ Group(modifier_key(key))("name")
|
||
+ Optional(comparitor_symbol("symbol") + term("value"))
|
||
)("modifier")
|
||
modifier.add_parse_action(_set_info)
|
||
return ZeroOrMore(modifier)("modifier_list")
|
||
|
||
def modifier_list2(key):
|
||
modifier = Dict(
|
||
Literal("/")
|
||
+ Group(modifier_key(key))("name")
|
||
+ Optional(comparitor_symbol("symbol") + term("value")),
|
||
asdict=True,
|
||
)("modifier")
|
||
modifier.add_parse_action(_set_info)
|
||
return ZeroOrMore(modifier)("modifier_list")
|
||
|
||
def modifier_list3(key):
|
||
modifier = Group( # this line is different from the others, must group to get results names
|
||
Dict(
|
||
Literal("/")
|
||
+ Group(modifier_key(key))("name")
|
||
+ Optional(comparitor_symbol("symbol") + term("value"))
|
||
)
|
||
)
|
||
modifier.add_parse_action(_set_info)
|
||
return ZeroOrMore(modifier)("modifier_list")
|
||
|
||
def modifier_list4(key):
|
||
modifier = Dict(
|
||
Literal("/")
|
||
+ Group(modifier_key(key))("name")
|
||
+ Optional(comparitor_symbol("symbol") + term("value")),
|
||
asdict=True,
|
||
)
|
||
modifier.add_parse_action(_set_info)
|
||
return ZeroOrMore(modifier)("modifier_list")
|
||
|
||
for modifier_list_fn in (
|
||
modifier_list1,
|
||
modifier_list2,
|
||
modifier_list3,
|
||
modifier_list4,
|
||
):
|
||
modifier_parser = modifier_list_fn("default")
|
||
|
||
result = modifier_parser.parse_string(
|
||
"/respectaccents/ignoreaccents", parse_all=True
|
||
)
|
||
for r in result:
|
||
print(r)
|
||
print(r.get("_info_"))
|
||
self.assertEqual([0, 15], [r["_info_"][1] for r in result])
|
||
|
||
def testParseResultsFromDict(self):
|
||
"""test helper classmethod ParseResults.from_dict()"""
|
||
|
||
dict = {
|
||
"first": "123",
|
||
"second": 456,
|
||
"third": {"threeStr": "789", "threeInt": 789},
|
||
}
|
||
name = "trios"
|
||
result = pp.ParseResults.from_dict(dict, name=name)
|
||
|
||
print(result.dump())
|
||
expected = {name: dict}
|
||
self.assertParseResultsEquals(
|
||
result,
|
||
expected_dict=expected,
|
||
msg="issue creating ParseResults.from _dict()",
|
||
)
|
||
|
||
def testParseResultsDir(self):
|
||
"""test dir(ParseResults)"""
|
||
|
||
dict = {"first": "123", "second": "456", "third": "789"}
|
||
name = "trios"
|
||
result = pp.ParseResults.from_dict(dict, name=name)
|
||
dir_result = dir(result)
|
||
|
||
print(dir_result)
|
||
self.assertIn(
|
||
name, dir_result, msg="name value wasn't returned by dir(ParseResults)"
|
||
)
|
||
self.assertIn(
|
||
"as_list", dir_result, msg="as_list was not returned by dir(ParseResults)"
|
||
)
|
||
|
||
def testParseResultsInsert(self):
|
||
"""test ParseResults.insert() with named tokens"""
|
||
|
||
from random import randint
|
||
|
||
result = pp.Word(pp.alphas)[...].parse_string(
|
||
"A B C D E F G H I J", parse_all=True
|
||
)
|
||
compare_list = result.as_list()
|
||
|
||
print(result)
|
||
print(compare_list)
|
||
|
||
for s in "abcdefghij":
|
||
index = randint(-5, 5)
|
||
result.insert(index, s)
|
||
compare_list.insert(index, s)
|
||
|
||
print(result)
|
||
print(compare_list)
|
||
|
||
self.assertParseResultsEquals(
|
||
result, compare_list, msg="issue with ParseResults.insert()"
|
||
)
|
||
|
||
def testParseResultsAddingSuppressedTokenWithResultsName(self):
|
||
parser = "aaa" + (pp.NoMatch() | pp.Suppress("-"))("B")
|
||
try:
|
||
dd = parser.parse_string("aaa -").as_dict()
|
||
except RecursionError:
|
||
self.fail("fail getting named result when empty")
|
||
|
||
def testParseResultsBool(self):
|
||
result = pp.Word(pp.alphas)[...].parse_string("AAA", parse_all=True)
|
||
self.assertTrue(result, "non-empty ParseResults evaluated as False")
|
||
|
||
result = pp.Word(pp.alphas)[...].parse_string("", parse_all=True)
|
||
self.assertFalse(result, "empty ParseResults evaluated as True")
|
||
|
||
result["A"] = 0
|
||
self.assertTrue(
|
||
result,
|
||
"ParseResults with empty list but containing a results name evaluated as False",
|
||
)
|
||
|
||
def testParseResultsWithAsListWithAndWithoutFlattening(self):
|
||
ppc = pp.common
|
||
|
||
# define a recursive grammar so we can easily build nested ParseResults
|
||
LPAR, RPAR = pp.Suppress.using_each("()")
|
||
fn_call = pp.Forward()
|
||
fn_arg = fn_call | ppc.identifier | ppc.number
|
||
fn_call <<= ppc.identifier + pp.Group(
|
||
LPAR + pp.Optional(pp.DelimitedList(fn_arg)) + RPAR
|
||
)
|
||
|
||
tests = [
|
||
("random()", ["random", []]),
|
||
("sin(theta)", ["sin", ["theta"]]),
|
||
("sin(rad(30))", ["sin", ["rad", [30]]]),
|
||
("sin(rad(30), rad(60, 180))", ["sin", ["rad", [30], "rad", [60, 180]]]),
|
||
(
|
||
"sin(rad(30), rad(60, 180), alpha)",
|
||
["sin", ["rad", [30], "rad", [60, 180], "alpha"]],
|
||
),
|
||
]
|
||
for test_string, expected in tests:
|
||
with self.subTest():
|
||
print(test_string)
|
||
observed = fn_call.parse_string(test_string, parse_all=True)
|
||
print(observed.as_list())
|
||
self.assertEqual(expected, observed.as_list())
|
||
print(observed.as_list(flatten=True))
|
||
self.assertEqual(flatten(expected), observed.as_list(flatten=True))
|
||
print()
|
||
|
||
def testParseResultsCopy(self):
|
||
expr = (
|
||
pp.Word(pp.nums)
|
||
+ pp.Group(pp.Word(pp.alphas)("key") + "=" + pp.Word(pp.nums)("value"))[...]
|
||
)
|
||
result = expr.parse_string("1 a=100 b=200 c=300")
|
||
print(result.dump())
|
||
|
||
r2 = result.copy()
|
||
print(r2.dump())
|
||
|
||
# check copy is different, but contained results is the same as in original
|
||
self.assertFalse(r2 is result, "copy failed")
|
||
self.assertTrue(r2[1] is result[1], "shallow copy failed")
|
||
|
||
# update result sub-element in place
|
||
result[1][0] = "z"
|
||
self.assertParseResultsEquals(
|
||
result,
|
||
expected_list=[
|
||
"1",
|
||
["z", "=", "100"],
|
||
["b", "=", "200"],
|
||
["c", "=", "300"],
|
||
],
|
||
)
|
||
|
||
# update contained results, verify list and dict contents are updated as expected
|
||
result[1][0] = result[1]["key"] = "q"
|
||
result[1]["xyz"] = 1000
|
||
print(result.dump())
|
||
self.assertParseResultsEquals(
|
||
result,
|
||
expected_list=[
|
||
"1",
|
||
["q", "=", "100"],
|
||
["b", "=", "200"],
|
||
["c", "=", "300"],
|
||
],
|
||
)
|
||
self.assertParseResultsEquals(
|
||
result[1], expected_dict={"key": "q", "value": "100", "xyz": 1000}
|
||
)
|
||
|
||
# verify that list and dict contents are the same in copy
|
||
self.assertParseResultsEquals(
|
||
r2,
|
||
expected_list=[
|
||
"1",
|
||
["q", "=", "100"],
|
||
["b", "=", "200"],
|
||
["c", "=", "300"],
|
||
],
|
||
)
|
||
self.assertParseResultsEquals(
|
||
r2[1], expected_dict={"key": "q", "value": "100", "xyz": 1000}
|
||
)
|
||
|
||
def testParseResultsCopyResultsNamesAreIndependent(self):
|
||
# copy() shares the contained results, but renumbering the offsets of
|
||
# one must not renumber the other's: insert/pop/del shift the recorded
|
||
# positions, and a shared list would shift them for both.
|
||
expr = pp.Word(pp.nums) + pp.Word(pp.nums)("b")
|
||
|
||
# mutating the copy must not disturb the original's names
|
||
result = expr.parse_string("1 2")
|
||
r2 = result.copy()
|
||
r2.insert(0, "X")
|
||
del result[0]
|
||
self.assertEqual("b", result.get_name())
|
||
|
||
# ...and the other way round
|
||
result = expr.parse_string("1 2")
|
||
r2 = result.copy()
|
||
result.insert(0, "X")
|
||
del r2[0]
|
||
self.assertEqual("b", r2.get_name())
|
||
|
||
def testParseResultsDeepcopy(self):
|
||
expr = (
|
||
pp.Word(pp.nums)
|
||
+ pp.Group(
|
||
pp.Word(pp.alphas)("key")
|
||
+ "="
|
||
+ pp.Group(
|
||
pp.Word(pp.nums)
|
||
)("value")
|
||
)[...]
|
||
)
|
||
result = expr.parse_string("1 a=100 b=200 c=300")
|
||
orig_elements = result._toklist[:]
|
||
|
||
r2 = result.deepcopy()
|
||
print(r2.dump())
|
||
|
||
# check copy and contained results are different from original
|
||
self.assertFalse(r2 is result, "copy failed")
|
||
self.assertFalse(r2[1] is result[1], "deep copy failed")
|
||
|
||
# check that the copy's list and dict entries are the same object
|
||
for i in (1, 2, 3):
|
||
self.assertTrue(r2[i][2] is r2[i].value)
|
||
|
||
# check copy and original are equal
|
||
self.assertEqual(result.as_dict(), r2.as_dict())
|
||
self.assertEqual(result.as_list(), r2.as_list())
|
||
|
||
# check original is unchanged
|
||
self.assertTrue(
|
||
all(
|
||
orig_element is result_element
|
||
for orig_element, result_element in zip(orig_elements, result._toklist)
|
||
)
|
||
)
|
||
|
||
# update contained results
|
||
result[1][0] = result[1]["key"] = "q"
|
||
result[1]["xyz"] = 1000
|
||
print(result.dump())
|
||
|
||
# verify that list and dict contents are unchanged in the copy
|
||
self.assertParseResultsEquals(
|
||
r2,
|
||
expected_list=[
|
||
"1",
|
||
["a", "=", ["100"]],
|
||
["b", "=", ["200"]],
|
||
["c", "=", ["300"]],
|
||
],
|
||
)
|
||
self.assertParseResultsEquals(r2[1], expected_dict={"key": "a", "value": ["100"]})
|
||
|
||
def testParseResultsDeepcopy2(self):
|
||
expr = (
|
||
pp.Word(pp.nums)
|
||
+ pp.Group(
|
||
pp.Word(pp.alphas)("key") + "=" + pp.Word(pp.nums)("value"), aslist=True
|
||
)[...]
|
||
)
|
||
result = expr.parse_string("1 a=100 b=200 c=300")
|
||
|
||
r2 = result.deepcopy()
|
||
print(r2.dump())
|
||
|
||
# check copy and contained results are different from original
|
||
self.assertFalse(r2 is result, "copy failed")
|
||
self.assertFalse(r2[1] is result[1], "deep copy failed")
|
||
|
||
# update contained results
|
||
result[1][0] = "q"
|
||
print(result.dump())
|
||
|
||
# verify that list and dict contents are unchanged in the copy
|
||
self.assertParseResultsEquals(
|
||
r2,
|
||
expected_list=[
|
||
"1",
|
||
["a", "=", "100"],
|
||
["b", "=", "200"],
|
||
["c", "=", "300"],
|
||
],
|
||
)
|
||
|
||
def testParseResultsDeepcopy3(self):
|
||
expr = (
|
||
pp.Word(pp.nums)
|
||
+ pp.Group(
|
||
(
|
||
pp.Word(pp.alphas)("key") + "=" + pp.Word(pp.nums)("value")
|
||
).add_parse_action(lambda t: tuple(t))
|
||
)[...]
|
||
)
|
||
result = expr.parse_string("1 a=100 b=200 c=300")
|
||
|
||
r2 = result.deepcopy()
|
||
print(r2.dump())
|
||
|
||
# check copy and contained results are different from original
|
||
self.assertFalse(r2 is result, "copy failed")
|
||
self.assertFalse(r2[1] is result[1], "deep copy failed")
|
||
|
||
# update contained results
|
||
result[1][0] = "q"
|
||
print(result.dump())
|
||
|
||
# verify that list and dict contents are unchanged in the copy
|
||
self.assertParseResultsEquals(
|
||
r2,
|
||
expected_list=[
|
||
"1",
|
||
[("a", "=", "100")],
|
||
[("b", "=", "200")],
|
||
[("c", "=", "300")],
|
||
],
|
||
)
|
||
|
||
def testParseResultsDeepcopy4(self):
|
||
# a top-level results name pointing at a mutable sub-ParseResults
|
||
# (the named token is also the list item) must be deep-copied, so the
|
||
# copy stays decoupled from the original whether reached by index or
|
||
# by name
|
||
expr = pp.Group(pp.Word(pp.nums)("a") + pp.Word(pp.nums)("b"))("grp") + pp.Word(
|
||
pp.alphas
|
||
)("w")
|
||
result = expr.parse_string("1 2 xyz")
|
||
|
||
r2 = result.deepcopy()
|
||
|
||
# named result and list item must be the same object within the copy,
|
||
# and both must differ from the original
|
||
self.assertTrue(r2["grp"] is r2[0], "named result diverged from list item")
|
||
self.assertFalse(
|
||
r2["grp"] is result["grp"], "deep copy failed for named result"
|
||
)
|
||
|
||
# mutate the original's named sub-result; the deep copy must not change
|
||
result["grp"][0] = result["grp"]["a"] = "999"
|
||
|
||
self.assertParseResultsEquals(
|
||
r2,
|
||
expected_list=[["1", "2"], "xyz"],
|
||
expected_dict={"grp": {"a": "1", "b": "2"}, "w": "xyz"},
|
||
)
|
||
self.assertEqual({"a": "1", "b": "2"}, r2["grp"].as_dict())
|
||
|
||
def testIgnoreString(self):
|
||
"""test ParserElement.ignore() passed a string arg"""
|
||
|
||
tst = "I like totally like love pickles"
|
||
expr = pp.Word(pp.alphas)[...].ignore("like")
|
||
result = expr.parse_string(tst, parse_all=True)
|
||
|
||
print(result)
|
||
expected = ["I", "totally", "love", "pickles"]
|
||
self.assertParseResultsEquals(result, expected, msg="issue with ignore(string)")
|
||
|
||
def testParseHTMLTags(self):
|
||
test = """
|
||
<BODY>
|
||
<BODY BGCOLOR="#00FFCC">
|
||
<BODY BGCOLOR="#00FFAA"/>
|
||
<BODY BGCOLOR='#00FFBB' FGCOLOR=black>
|
||
<BODY/>
|
||
</BODY>
|
||
"""
|
||
results = [
|
||
("startBody", False, "", ""),
|
||
("startBody", False, "#00FFCC", ""),
|
||
("startBody", True, "#00FFAA", ""),
|
||
("startBody", False, "#00FFBB", "black"),
|
||
("startBody", True, "", ""),
|
||
("endBody", False, "", ""),
|
||
]
|
||
|
||
bodyStart, bodyEnd = pp.make_html_tags("BODY")
|
||
resIter = iter(results)
|
||
for t, s, e in (bodyStart | bodyEnd).scan_string(test):
|
||
print(test[s:e], "->", t)
|
||
(expectedType, expectedEmpty, expectedBG, expectedFG) = next(resIter)
|
||
|
||
print(t.dump())
|
||
if "startBody" in t:
|
||
self.assertEqual(
|
||
expectedEmpty,
|
||
bool(t.empty),
|
||
f"expected {expectedEmpty and 'empty' or 'not empty'} token,"
|
||
f" got {t.empty and 'empty' or 'not empty'}",
|
||
)
|
||
self.assertEqual(
|
||
expectedBG,
|
||
t.bgcolor,
|
||
f"failed to match BGCOLOR, expected {expectedBG}, got {t.bgcolor}",
|
||
)
|
||
self.assertEqual(
|
||
expectedFG,
|
||
t.fgcolor,
|
||
f"failed to match FGCOLOR, expected {expectedFG}, got {t.bgcolor}",
|
||
)
|
||
elif "endBody" in t:
|
||
print("end tag")
|
||
pass
|
||
else:
|
||
print("BAD!!!")
|
||
|
||
def testSetParseActionUncallableErr(self):
|
||
"""raise a TypeError in set_parse_action() by adding uncallable arg"""
|
||
|
||
expr = pp.Literal("A")("Achar")
|
||
uncallable = 12
|
||
|
||
with self.assertRaises(TypeError):
|
||
expr.set_parse_action(uncallable)
|
||
|
||
res = expr.parse_string("A", parse_all=True)
|
||
print(res.dump())
|
||
|
||
def testMulWithNegativeNumber(self):
|
||
"""raise a ValueError in __mul__ by multiplying a negative number"""
|
||
|
||
with self.assertRaises(ValueError):
|
||
pp.Literal("A")("Achar") * (-1)
|
||
|
||
def testMulWithEllipsis(self):
|
||
"""multiply an expression with Ellipsis as ``expr * ...`` to match ZeroOrMore"""
|
||
|
||
expr = pp.Literal("A")("Achar") * ...
|
||
res = expr.parse_string("A", parse_all=True)
|
||
self.assertEqual(
|
||
["A"], res.as_list(), "expected expr * ... to match ZeroOrMore"
|
||
)
|
||
print(res.dump())
|
||
|
||
def testUpcaseDowncaseUnicode(self):
|
||
import sys
|
||
|
||
ppu = pp.pyparsing_unicode
|
||
|
||
a = "\u00bfC\u00f3mo esta usted?"
|
||
if not JYTHON_ENV:
|
||
ualphas = ppu.alphas
|
||
else:
|
||
ualphas = "".join(
|
||
chr(i)
|
||
for i in list(range(0xD800)) + list(range(0xE000, sys.maxunicode))
|
||
if chr(i).isalpha()
|
||
)
|
||
uword = pp.Word(ualphas).set_parse_action(ppc.upcase_tokens)
|
||
|
||
print = lambda *args: None
|
||
print(uword.search_string(a))
|
||
|
||
uword = pp.Word(ualphas).set_parse_action(ppc.downcase_tokens)
|
||
|
||
print(uword.search_string(a))
|
||
|
||
kw = pp.Keyword("mykey", caseless=True).set_parse_action(ppc.upcase_tokens)(
|
||
"rname"
|
||
)
|
||
ret = kw.parse_string("mykey", parse_all=True)
|
||
print(ret.rname)
|
||
self.assertEqual(
|
||
"MYKEY", ret.rname, "failed to upcase with named result (pyparsing_common)"
|
||
)
|
||
|
||
kw = pp.Keyword("MYKEY", caseless=True).set_parse_action(ppc.downcase_tokens)(
|
||
"rname"
|
||
)
|
||
ret = kw.parse_string("mykey", parse_all=True)
|
||
print(ret.rname)
|
||
self.assertEqual("mykey", ret.rname, "failed to upcase with named result")
|
||
|
||
if not IRON_PYTHON_ENV:
|
||
# test html data
|
||
html = "<TR class=maintxt bgColor=#ffffff> \
|
||
<TD vAlign=top>Производитель, модель</TD> \
|
||
<TD vAlign=top><STRONG>BenQ-Siemens CF61</STRONG></TD> \
|
||
" # .decode('utf-8')
|
||
|
||
# 'Manufacturer, model
|
||
text_manuf = "Производитель, модель"
|
||
manufacturer = pp.Literal(text_manuf)
|
||
|
||
td_start, td_end = pp.make_html_tags("td")
|
||
manuf_body = (
|
||
td_start.suppress()
|
||
+ manufacturer
|
||
+ pp.SkipTo(td_end)("cells*")
|
||
+ td_end.suppress()
|
||
)
|
||
|
||
def testRegexDeferredCompile(self):
|
||
"""test deferred compilation of Regex patterns"""
|
||
re_expr = pp.Regex(r"[A-Z]*")
|
||
self.assertIsNone(
|
||
re_expr._may_return_empty,
|
||
"failed to initialize _may_return_empty flag to None",
|
||
)
|
||
self.assertEqual(re_expr._re, None)
|
||
|
||
compiled = re_expr.re
|
||
self.assertTrue(
|
||
re_expr._may_return_empty, "failed to set _may_return_empty flag to True"
|
||
)
|
||
self.assertEqual(re_expr._re, compiled)
|
||
|
||
non_empty_re_expr = pp.Regex(r"[A-Z]+")
|
||
self.assertIsNone(
|
||
non_empty_re_expr._may_return_empty,
|
||
"failed to initialize _may_return_empty flag to None",
|
||
)
|
||
self.assertEqual(non_empty_re_expr._re, None)
|
||
|
||
compiled = non_empty_re_expr.re
|
||
self.assertFalse(
|
||
non_empty_re_expr._may_return_empty,
|
||
"failed to set _may_return_empty flag to False",
|
||
)
|
||
self.assertEqual(non_empty_re_expr._re, compiled)
|
||
|
||
def testRegexDeferredCompileCommonHtmlEntity(self):
|
||
# this is the most important expression to defer, because it takes a long time to compile
|
||
perf_test_common_html_entity = pp.common_html_entity()
|
||
|
||
# force internal var to None, to simulate a fresh instance
|
||
perf_test_common_html_entity._re = None
|
||
|
||
# just how long does this take anyway?
|
||
from time import perf_counter
|
||
|
||
start = perf_counter()
|
||
perf_test_common_html_entity.re # noqa
|
||
elapsed = perf_counter() - start
|
||
print(f"elapsed time to compile common_html_entity: {elapsed:.4f} sec")
|
||
|
||
def testParseUsingRegex(self):
|
||
signedInt = pp.Regex(r"[-+][0-9]+")
|
||
unsignedInt = pp.Regex(r"[0-9]+")
|
||
simpleString = pp.Regex(r'("[^\"]*")|(\'[^\']*\')')
|
||
namedGrouping = pp.Regex(r'("(?P<content>[^\"]*)")')
|
||
compiledRE = pp.Regex(re.compile(r"[A-Z]+"))
|
||
|
||
def testMatch(expression, instring, shouldPass, expectedString=None):
|
||
if shouldPass:
|
||
try:
|
||
result = expression.parse_string(instring, parse_all=False)
|
||
print(f"{repr(expression)} correctly matched {repr(instring)}")
|
||
if expectedString != result[0]:
|
||
print("\tbut failed to match the pattern as expected:")
|
||
print(
|
||
f"\tproduced {repr(result[0])} instead of {repr(expectedString)}"
|
||
)
|
||
return False
|
||
return True
|
||
except pp.ParseException:
|
||
print(f"{expression!r} incorrectly failed to match {instring!r}")
|
||
else:
|
||
try:
|
||
result = expression.parse_string(instring, parse_all=False)
|
||
print(f"{expression!r} incorrectly matched {instring!r}")
|
||
print(f"\tproduced {result[0]!r} as a result")
|
||
except pp.ParseException:
|
||
print(f"{expression!r} correctly failed to match {instring!r}")
|
||
return True
|
||
return False
|
||
|
||
# These should fail
|
||
for i, (test_expr, test_string) in enumerate(
|
||
[
|
||
(signedInt, "1234 foo"),
|
||
(signedInt, " +foo"),
|
||
(unsignedInt, "abc"),
|
||
(unsignedInt, "+123 foo"),
|
||
(simpleString, "foo"),
|
||
(simpleString, "\"foo bar'"),
|
||
(simpleString, "'foo bar\""),
|
||
(compiledRE, "blah"),
|
||
],
|
||
start=1,
|
||
):
|
||
with self.subTest(test_expr=test_expr, test_string=test_string):
|
||
self.assertTrue(
|
||
testMatch(
|
||
test_expr,
|
||
test_string,
|
||
False,
|
||
),
|
||
f"Re: ({i}) passed, expected fail",
|
||
)
|
||
|
||
# These should pass
|
||
for i, (test_expr, test_string, expected_match) in enumerate(
|
||
[
|
||
(signedInt, " +123", "+123"),
|
||
(signedInt, "+123", "+123"),
|
||
(signedInt, "+123 foo", "+123"),
|
||
(signedInt, "-0 foo", "-0"),
|
||
(unsignedInt, "123 foo", "123"),
|
||
(unsignedInt, "0 foo", "0"),
|
||
(simpleString, '"foo"', '"foo"'),
|
||
(simpleString, "'foo bar' baz", "'foo bar'"),
|
||
(compiledRE, "BLAH", "BLAH"),
|
||
(namedGrouping, '"foo bar" baz', '"foo bar"'),
|
||
],
|
||
start=i + 1,
|
||
):
|
||
with self.subTest(test_expr=test_expr, test_string=test_string):
|
||
self.assertTrue(
|
||
testMatch(
|
||
test_expr,
|
||
test_string,
|
||
True,
|
||
expected_match,
|
||
),
|
||
f"Re: ({i}) failed, expected pass",
|
||
)
|
||
|
||
ret = namedGrouping.parse_string('"zork" blah', parse_all=False)
|
||
print(ret)
|
||
print(list(ret.items()))
|
||
print(ret.content)
|
||
self.assertEqual("zork", ret.content, "named group lookup failed")
|
||
|
||
self.assertEqual(
|
||
simpleString.parse_string('"zork" blah', parse_all=False)[0],
|
||
ret[0],
|
||
"Regex not properly returning ParseResults for named vs. unnamed groups",
|
||
)
|
||
|
||
try:
|
||
print("lets try an invalid RE")
|
||
invRe = pp.Regex("(\"[^\"]*\")|('[^']*'").re
|
||
except ValueError as e:
|
||
print("successfully rejected an invalid RE:", end=" ")
|
||
print(e)
|
||
else:
|
||
self.fail("failed to reject invalid RE")
|
||
|
||
with self.assertRaises(
|
||
ValueError, msg="failed to warn empty string passed to Regex"
|
||
):
|
||
pp.Regex("").re # noqa
|
||
|
||
def testRegexAsType(self):
|
||
test_str = "sldkjfj 123 456 lsdfkj"
|
||
|
||
print("return as list of match groups")
|
||
expr = pp.Regex(r"\w+ (\d+) (\d+) (\w+)", as_group_list=True)
|
||
expected_group_list = [tuple(test_str.split()[1:])]
|
||
result = expr.parse_string(test_str, parse_all=True)
|
||
print(result.dump())
|
||
print(expected_group_list)
|
||
self.assertParseResultsEquals(
|
||
result,
|
||
expected_list=expected_group_list,
|
||
msg="incorrect group list returned by Regex)",
|
||
)
|
||
|
||
print("return as re.match instance")
|
||
expr = pp.Regex(
|
||
r"\w+ (?P<num1>\d+) (?P<num2>\d+) (?P<last_word>\w+)", as_match=True
|
||
)
|
||
result = expr.parse_string(test_str, parse_all=True)
|
||
print(result.dump())
|
||
print(result[0].groups())
|
||
print(expected_group_list)
|
||
self.assertEqual(
|
||
{"num1": "123", "num2": "456", "last_word": "lsdfkj"},
|
||
result[0].groupdict(),
|
||
"invalid group dict from Regex(as_match=True)",
|
||
)
|
||
self.assertEqual(
|
||
expected_group_list[0],
|
||
result[0].groups(),
|
||
"incorrect group list returned by Regex(as_match)",
|
||
)
|
||
|
||
def testRegexSub(self):
|
||
print("test sub with string")
|
||
expr = pp.Regex(r"<title>").sub("'Richard III'")
|
||
result = expr.transform_string("This is the title: <title>")
|
||
print(result)
|
||
self.assertEqual(
|
||
"This is the title: 'Richard III'",
|
||
result,
|
||
"incorrect Regex.sub result with simple string",
|
||
)
|
||
|
||
print("test sub with re string")
|
||
expr = pp.Regex(r"([Hh]\d):\s*(.*)").sub(r"<\1>\2</\1>")
|
||
result = expr.transform_string(
|
||
"h1: This is the main heading\nh2: This is the sub-heading"
|
||
)
|
||
print(result)
|
||
self.assertEqual(
|
||
"<h1>This is the main heading</h1>\n<h2>This is the sub-heading</h2>",
|
||
result,
|
||
"incorrect Regex.sub result with re string",
|
||
)
|
||
|
||
print("test sub with re string (Regex returns re.match)")
|
||
expr = pp.Regex(r"([Hh]\d):\s*(.*)", as_match=True).sub(r"<\1>\2</\1>")
|
||
result = expr.transform_string(
|
||
"h1: This is the main heading\nh2: This is the sub-heading"
|
||
)
|
||
print(result)
|
||
self.assertEqual(
|
||
"<h1>This is the main heading</h1>\n<h2>This is the sub-heading</h2>",
|
||
result,
|
||
"incorrect Regex.sub result with re string",
|
||
)
|
||
|
||
print("test sub with callable that return str")
|
||
expr = pp.Regex(r"<(.*?)>").sub(lambda m: m.group(1).upper())
|
||
result = expr.transform_string("I want this in upcase: <what? what?>")
|
||
print(result)
|
||
self.assertEqual(
|
||
"I want this in upcase: WHAT? WHAT?",
|
||
result,
|
||
"incorrect Regex.sub result with callable",
|
||
)
|
||
|
||
with self.assertRaises(TypeError):
|
||
pp.Regex(r"<(.*?)>", as_match=True).sub(lambda m: m.group(1).upper())
|
||
|
||
with self.assertRaises(TypeError):
|
||
pp.Regex(r"<(.*?)>", as_group_list=True).sub(lambda m: m.group(1).upper())
|
||
|
||
with self.assertRaises(TypeError):
|
||
pp.Regex(r"<(.*?)>", as_group_list=True).sub("")
|
||
|
||
def testRegexInvalidType(self):
|
||
"""test Regex of an invalid type"""
|
||
|
||
with self.assertRaises(TypeError, msg="issue with Regex of type int"):
|
||
expr = pp.Regex(12)
|
||
|
||
def testRegexLoopPastEndOfString(self):
|
||
"""test Regex matching after end of string"""
|
||
NL = pp.LineEnd().suppress()
|
||
empty_line = pp.rest_of_line() + NL
|
||
result = empty_line[1, 10].parse_string("\n\n")
|
||
self.assertEqual(3, len(result))
|
||
|
||
def testPrecededBy(self):
|
||
num = pp.Word(pp.nums).set_parse_action(lambda t: int(t[0]))
|
||
interesting_num = pp.PrecededBy(pp.Char("abc")("prefix*")) + num
|
||
semi_interesting_num = pp.PrecededBy("_") + num
|
||
crazy_num = pp.PrecededBy(pp.Word("^", "$%^")("prefix*"), 10) + num
|
||
boring_num = ~pp.PrecededBy(pp.Char("abc_$%^" + pp.nums)) + num
|
||
very_boring_num = pp.PrecededBy(pp.WordStart()) + num
|
||
finicky_num = pp.PrecededBy(pp.Word("^", "$%^"), retreat=3) + num
|
||
|
||
s = "c384 b8324 _9293874 _293 404 $%^$^%$2939"
|
||
print(s)
|
||
for expr, expected_list, expected_dict in [
|
||
(interesting_num, [384, 8324], {"prefix": ["c", "b"]}),
|
||
(semi_interesting_num, [9293874, 293], {}),
|
||
(boring_num, [404], {}),
|
||
(crazy_num, [2939], {"prefix": ["^%$"]}),
|
||
(finicky_num, [2939], {}),
|
||
(very_boring_num, [404], {}),
|
||
]:
|
||
# print(expr.search_string(s))
|
||
result = sum(expr.search_string(s))
|
||
print(result.dump())
|
||
self.assertParseResultsEquals(result, expected_list, expected_dict)
|
||
|
||
# infinite loop test - from Issue #127
|
||
string_test = "notworking"
|
||
# negs = pp.Or(['not', 'un'])('negs')
|
||
negs_pb = pp.PrecededBy("not", retreat=100)("negs_lb")
|
||
# negs_pb = pp.PrecededBy(negs, retreat=100)('negs_lb')
|
||
pattern = (negs_pb + pp.Literal("working"))("main")
|
||
|
||
results = pattern.search_string(string_test)
|
||
try:
|
||
print(results.dump())
|
||
except RecursionError:
|
||
self.fail("got maximum excursion limit exception")
|
||
else:
|
||
print("got maximum excursion limit exception")
|
||
|
||
def testCountedArray(self):
|
||
testString = "2 5 7 6 0 1 2 3 4 5 0 3 5 4 3"
|
||
|
||
integer = pp.Word(pp.nums).set_parse_action(lambda t: int(t[0]))
|
||
countedField = pp.counted_array(integer)
|
||
|
||
r = pp.OneOrMore(pp.Group(countedField)).parse_string(
|
||
testString, parse_all=True
|
||
)
|
||
print(testString)
|
||
print(r)
|
||
|
||
self.assertParseResultsEquals(
|
||
r, expected_list=[[5, 7], [0, 1, 2, 3, 4, 5], [], [5, 4, 3]]
|
||
)
|
||
|
||
# addresses bug raised by Ralf Vosseler
|
||
def testCountedArrayTest2(self):
|
||
testString = "2 5 7 6 0 1 2 3 4 5 0 3 5 4 3"
|
||
|
||
integer = pp.Word(pp.nums).set_parse_action(lambda t: int(t[0]))
|
||
countedField = pp.counted_array(integer)
|
||
|
||
dummy = pp.Word("A")
|
||
r = pp.OneOrMore(pp.Group(dummy ^ countedField)).parse_string(
|
||
testString, parse_all=True
|
||
)
|
||
print(testString)
|
||
print(r)
|
||
|
||
self.assertParseResultsEquals(
|
||
r, expected_list=[[5, 7], [0, 1, 2, 3, 4, 5], [], [5, 4, 3]]
|
||
)
|
||
|
||
def testCountedArrayTest3(self):
|
||
int_chars = "_" + pp.alphas
|
||
array_counter = pp.Word(int_chars).set_parse_action(
|
||
lambda t: int_chars.index(t[0])
|
||
)
|
||
|
||
# 123456789012345678901234567890
|
||
testString = "B 5 7 F 0 1 2 3 4 5 _ C 5 4 3"
|
||
|
||
integer = pp.Word(pp.nums).set_parse_action(lambda t: int(t[0]))
|
||
countedField = pp.counted_array(integer, int_expr=array_counter)
|
||
|
||
r = pp.OneOrMore(pp.Group(countedField)).parse_string(
|
||
testString, parse_all=True
|
||
)
|
||
print(testString)
|
||
print(r)
|
||
|
||
self.assertParseResultsEquals(
|
||
r, expected_list=[[5, 7], [0, 1, 2, 3, 4, 5], [], [5, 4, 3]]
|
||
)
|
||
|
||
def testCountedArrayTest4(self):
|
||
ppc = pp.pyparsing_common
|
||
|
||
# array counter contains several fields - first field *must* be the number of
|
||
# items in the array
|
||
# - number of elements
|
||
# - type of elements
|
||
# - source of elements
|
||
counter_with_metadata = (
|
||
ppc.integer("count") + ppc.identifier("type") + ppc.identifier("source")
|
||
)
|
||
|
||
countedField = pp.counted_array(
|
||
pp.Word(pp.alphanums), int_expr=counter_with_metadata
|
||
)
|
||
|
||
testString = (
|
||
"5 string input item1 item2 item3 item4 item5 0 int user 2 int file 3 8"
|
||
)
|
||
r = pp.Group(countedField("items"))[...].parse_string(
|
||
testString, parse_all=True
|
||
)
|
||
|
||
print(testString)
|
||
print(r.dump())
|
||
print(f"type = {r.type!r}")
|
||
print(f"source = {r.source!r}")
|
||
|
||
self.assertParseResultsEquals(
|
||
r,
|
||
expected_list=[
|
||
["item1", "item2", "item3", "item4", "item5"],
|
||
[],
|
||
["3", "8"],
|
||
],
|
||
)
|
||
|
||
self.assertParseResultsEquals(
|
||
r[0],
|
||
expected_dict={
|
||
"count": 5,
|
||
"source": "input",
|
||
"type": "string",
|
||
"items": ["item1", "item2", "item3", "item4", "item5"],
|
||
},
|
||
)
|
||
|
||
# parse with additional fields between the count and the actual list items
|
||
count_with_metadata = ppc.integer + pp.Word(pp.alphas)("type")
|
||
typed_array = pp.counted_array(
|
||
pp.Word(pp.alphanums), int_expr=count_with_metadata
|
||
)("items")
|
||
result = typed_array.parse_string("3 bool True True False", parse_all=True)
|
||
print(result.dump())
|
||
|
||
self.assertParseResultsEquals(
|
||
result,
|
||
expected_list=["True", "True", "False"],
|
||
expected_dict={"type": "bool", "items": ["True", "True", "False"]},
|
||
)
|
||
|
||
def testLineStart(self):
|
||
pass_tests = [
|
||
"""\
|
||
AAA
|
||
BBB
|
||
""",
|
||
"""\
|
||
AAA...
|
||
BBB
|
||
""",
|
||
]
|
||
fail_tests = [
|
||
"""\
|
||
AAA...
|
||
...BBB
|
||
""",
|
||
"""\
|
||
AAA BBB
|
||
""",
|
||
]
|
||
|
||
# cleanup test strings
|
||
pass_tests = [
|
||
"\n".join(s.lstrip() for s in t.splitlines()).replace(".", " ")
|
||
for t in pass_tests
|
||
]
|
||
fail_tests = [
|
||
"\n".join(s.lstrip() for s in t.splitlines()).replace(".", " ")
|
||
for t in fail_tests
|
||
]
|
||
|
||
test_patt = pp.Word("A") - pp.LineStart() + pp.Word("B")
|
||
print(test_patt.streamline())
|
||
success, _ = test_patt.run_tests(pass_tests)
|
||
self.assertTrue(success, "failed LineStart passing tests (1)")
|
||
|
||
success, _ = test_patt.run_tests(fail_tests, failure_tests=True)
|
||
self.assertTrue(success, "failed LineStart failure mode tests (1)")
|
||
|
||
with ppt.reset_pyparsing_context():
|
||
print(r"no \n in default whitespace chars")
|
||
pp.ParserElement.set_default_whitespace_chars(" ")
|
||
|
||
test_patt = pp.Word("A") - pp.LineStart() + pp.Word("B")
|
||
print(test_patt.streamline())
|
||
# should fail the pass tests too, since \n is no longer valid whitespace and we aren't parsing for it
|
||
success, _ = test_patt.run_tests(pass_tests, failure_tests=True)
|
||
self.assertTrue(success, "failed LineStart passing tests (2)")
|
||
|
||
success, _ = test_patt.run_tests(fail_tests, failure_tests=True)
|
||
self.assertTrue(success, "failed LineStart failure mode tests (2)")
|
||
|
||
test_patt = (
|
||
pp.Word("A")
|
||
- pp.LineEnd().suppress()
|
||
+ pp.LineStart()
|
||
+ pp.Word("B")
|
||
+ pp.LineEnd().suppress()
|
||
)
|
||
print(test_patt.streamline())
|
||
success, _ = test_patt.run_tests(pass_tests)
|
||
self.assertTrue(success, "failed LineStart passing tests (3)")
|
||
|
||
success, _ = test_patt.run_tests(fail_tests, failure_tests=True)
|
||
self.assertTrue(success, "failed LineStart failure mode tests (3)")
|
||
|
||
def testLineStart2(self):
|
||
test = """\
|
||
AAA 1
|
||
AAA 2
|
||
|
||
AAA
|
||
|
||
B AAA
|
||
|
||
"""
|
||
|
||
test = dedent(test)
|
||
print(pp.testing.with_line_numbers(test))
|
||
|
||
print("normal parsing")
|
||
for t, s, e in (pp.LineStart() + "AAA").scan_string(test):
|
||
print(s, e, pp.lineno(s, test), pp.line(s, test), repr(t))
|
||
print()
|
||
self.assertEqual(
|
||
"A", t[0][0], "failed LineStart with insignificant newlines"
|
||
)
|
||
|
||
print(r"parsing without \n in whitespace chars")
|
||
with ppt.reset_pyparsing_context():
|
||
pp.ParserElement.set_default_whitespace_chars(" ")
|
||
for t, s, e in (pp.LineStart() + "AAA").scan_string(test):
|
||
print(s, e, pp.lineno(s, test), pp.line(s, test), repr(test[s]))
|
||
print()
|
||
self.assertEqual(
|
||
"A", t[0][0], "failed LineStart with insignificant newlines"
|
||
)
|
||
|
||
def testLineStartWithLeadingSpaces(self):
|
||
# testing issue #272
|
||
# reverted in 3.0.2 - LineStart() + expr will match expr even if there
|
||
# are leading spaces. To force "only at column 1" matching, use
|
||
# AtLineStart(expr).
|
||
instring = dedent(
|
||
"""
|
||
a
|
||
b
|
||
c
|
||
d
|
||
e
|
||
f
|
||
g
|
||
"""
|
||
)
|
||
print(pp.testing.with_line_numbers(instring))
|
||
|
||
alpha_line = (
|
||
pp.LineStart().leave_whitespace()
|
||
+ pp.Word(pp.alphas)
|
||
+ pp.LineEnd().suppress()
|
||
)
|
||
|
||
tests = [
|
||
alpha_line,
|
||
pp.Group(alpha_line),
|
||
alpha_line | pp.Word("_"),
|
||
alpha_line | alpha_line,
|
||
pp.MatchFirst([alpha_line, alpha_line]),
|
||
alpha_line ^ pp.Word("_"),
|
||
alpha_line ^ alpha_line,
|
||
pp.Or([alpha_line, pp.Word("_")]),
|
||
pp.LineStart() + pp.Word(pp.alphas) + pp.LineEnd().suppress(),
|
||
pp.And([pp.LineStart(), pp.Word(pp.alphas), pp.LineEnd().suppress()]),
|
||
]
|
||
fails = []
|
||
for test in tests:
|
||
print(test.search_string(instring))
|
||
if ["a", "b", "c", "d", "e", "f", "g"] != flatten(
|
||
sum(test.search_string(instring)).as_list()
|
||
):
|
||
fails.append(test)
|
||
if fails:
|
||
self.fail(
|
||
"failed LineStart tests:\n{}".format(
|
||
"\n".join(str(expr) for expr in fails)
|
||
)
|
||
)
|
||
|
||
def testAtLineStart(self):
|
||
test = dedent(
|
||
"""\
|
||
AAA this line
|
||
AAA and this line
|
||
AAA but not this one
|
||
B AAA and definitely not this one
|
||
"""
|
||
)
|
||
|
||
expr = pp.AtLineStart("AAA") + pp.rest_of_line
|
||
for t in expr.search_string(test):
|
||
print(t)
|
||
|
||
self.assertEqual(
|
||
["AAA", " this line", "AAA", " and this line"],
|
||
sum(expr.search_string(test)).as_list(),
|
||
)
|
||
|
||
def testStringStart(self):
|
||
self.assertParseAndCheckList(
|
||
pp.StringStart() + pp.Word(pp.nums), "123", ["123"]
|
||
)
|
||
self.assertParseAndCheckList(
|
||
pp.StringStart() + pp.Word(pp.nums), " 123", ["123"]
|
||
)
|
||
self.assertParseAndCheckList(pp.StringStart() + "123", "123", ["123"])
|
||
self.assertParseAndCheckList(pp.StringStart() + "123", " 123", ["123"])
|
||
self.assertParseAndCheckList(pp.AtStringStart(pp.Word(pp.nums)), "123", ["123"])
|
||
|
||
self.assertParseAndCheckList(pp.AtStringStart("123"), "123", ["123"])
|
||
|
||
with self.assertRaisesParseException():
|
||
pp.AtStringStart(pp.Word(pp.nums)).parse_string(" 123")
|
||
|
||
with self.assertRaisesParseException():
|
||
pp.AtStringStart("123").parse_string(" 123")
|
||
|
||
def testStringStartAndLineStartInsideAnd(self):
|
||
# fmt: off
|
||
P_MTARG = (
|
||
pp.StringStart()
|
||
+ pp.Word("abcde")
|
||
+ pp.StringEnd()
|
||
)
|
||
|
||
P_MTARG2 = (
|
||
pp.LineStart()
|
||
+ pp.Word("abcde")
|
||
+ pp.StringEnd()
|
||
)
|
||
|
||
P_MTARG3 = (
|
||
pp.AtLineStart(pp.Word("abcde"))
|
||
+ pp.StringEnd()
|
||
)
|
||
# fmt: on
|
||
|
||
def test(expr, string):
|
||
expr.streamline()
|
||
print(expr, repr(string), end=" ")
|
||
print(expr.parse_string(string))
|
||
|
||
test(P_MTARG, "aaa")
|
||
test(P_MTARG2, "aaa")
|
||
test(P_MTARG2, "\naaa")
|
||
test(P_MTARG2, " aaa")
|
||
test(P_MTARG2, "\n aaa")
|
||
|
||
with self.assertRaisesParseException():
|
||
test(P_MTARG3, " aaa")
|
||
with self.assertRaisesParseException():
|
||
test(P_MTARG3, "\n aaa")
|
||
|
||
def testLineAndStringEnd(self):
|
||
NLs = pp.OneOrMore(pp.line_end)
|
||
bnf1 = pp.DelimitedList(pp.Word(pp.alphanums).leave_whitespace(), NLs)
|
||
bnf2 = pp.Word(pp.alphanums) + pp.string_end
|
||
bnf3 = pp.Word(pp.alphanums) + pp.SkipTo(pp.string_end)
|
||
tests = [
|
||
("testA\ntestB\ntestC\n", ["testA", "testB", "testC"]),
|
||
("testD\ntestE\ntestF", ["testD", "testE", "testF"]),
|
||
("a", ["a"]),
|
||
]
|
||
|
||
for test, expected in tests:
|
||
res1 = bnf1.parse_string(test, parse_all=True)
|
||
print(res1, "=?", expected)
|
||
self.assertParseResultsEquals(
|
||
res1,
|
||
expected_list=expected,
|
||
msg=f"Failed line_end/string_end test (1): {test!r} -> {res1}",
|
||
)
|
||
|
||
res2 = bnf2.search_string(test)[0]
|
||
print(res2, "=?", expected[-1:])
|
||
self.assertParseResultsEquals(
|
||
res2,
|
||
expected_list=expected[-1:],
|
||
msg=f"Failed line_end/string_end test (2): {test!r} -> {res2}",
|
||
)
|
||
|
||
res3 = bnf3.parse_string(test, parse_all=True)
|
||
first = res3[0]
|
||
rest = res3[1]
|
||
# ~ print res3.dump()
|
||
print(repr(rest), "=?", repr(test[len(first) + 1 :]))
|
||
self.assertEqual(
|
||
rest,
|
||
test[len(first) + 1 :],
|
||
msg=f"Failed line_end/string_end test (3): {test!r} -> {res3.as_list()}",
|
||
)
|
||
print()
|
||
|
||
k = pp.Regex(r"a+", flags=re.S + re.M)
|
||
k = k.parse_with_tabs()
|
||
k = k.leave_whitespace()
|
||
|
||
tests = [
|
||
(r"aaa", ["aaa"]),
|
||
(r"\naaa", None),
|
||
(r"a\naa", None),
|
||
(r"aaa\n", None),
|
||
]
|
||
for i, (src, expected) in enumerate(tests):
|
||
with self.subTest("", src=src, expected=expected):
|
||
print(i, repr(src).replace("\\\\", "\\"), end=" ")
|
||
if expected is None:
|
||
with self.assertRaisesParseException():
|
||
k.parse_string(src, parse_all=True)
|
||
else:
|
||
res = k.parse_string(src, parse_all=True)
|
||
self.assertParseResultsEquals(
|
||
res, expected, msg=f"Failed on parse_all=True test {i}"
|
||
)
|
||
|
||
def testVariableParseActionArgs(self):
|
||
pa3 = lambda s, l, t: t
|
||
pa2 = lambda l, t: t
|
||
pa1 = lambda t: t
|
||
pa0 = lambda: None
|
||
|
||
class Callable3:
|
||
def __call__(self, s, l, t):
|
||
return t
|
||
|
||
class Callable2:
|
||
def __call__(self, l, t):
|
||
return t
|
||
|
||
class Callable1:
|
||
def __call__(self, t):
|
||
return t
|
||
|
||
class Callable0:
|
||
def __call__(self):
|
||
return
|
||
|
||
class CallableS3:
|
||
@staticmethod
|
||
def __call__(s, l, t):
|
||
return t
|
||
|
||
class CallableS2:
|
||
@staticmethod
|
||
def __call__(l, t):
|
||
return t
|
||
|
||
class CallableS1:
|
||
@staticmethod
|
||
def __call__(t):
|
||
return t
|
||
|
||
class CallableS0:
|
||
@staticmethod
|
||
def __call__():
|
||
return
|
||
|
||
class CallableC3:
|
||
@classmethod
|
||
def __call__(cls, s, l, t):
|
||
return t
|
||
|
||
class CallableC2:
|
||
@classmethod
|
||
def __call__(cls, l, t):
|
||
return t
|
||
|
||
class CallableC1:
|
||
@classmethod
|
||
def __call__(cls, t):
|
||
return t
|
||
|
||
class CallableC0:
|
||
@classmethod
|
||
def __call__(cls):
|
||
return
|
||
|
||
class parseActionHolder:
|
||
@staticmethod
|
||
def pa3(s, l, t):
|
||
return t
|
||
|
||
@staticmethod
|
||
def pa2(l, t):
|
||
return t
|
||
|
||
@staticmethod
|
||
def pa1(t):
|
||
return t
|
||
|
||
@staticmethod
|
||
def pa0():
|
||
return
|
||
|
||
def paArgs(*args):
|
||
print(args)
|
||
return args[2]
|
||
|
||
class ClassAsPA0:
|
||
def __init__(self):
|
||
pass
|
||
|
||
def __str__(self):
|
||
return "A"
|
||
|
||
class ClassAsPA1:
|
||
def __init__(self, t):
|
||
print("making a ClassAsPA1")
|
||
self.t = t
|
||
|
||
def __str__(self):
|
||
return self.t[0]
|
||
|
||
class ClassAsPA2:
|
||
def __init__(self, l, t):
|
||
self.t = t
|
||
|
||
def __str__(self):
|
||
return self.t[0]
|
||
|
||
class ClassAsPA3:
|
||
def __init__(self, s, l, t):
|
||
self.t = t
|
||
|
||
def __str__(self):
|
||
return self.t[0]
|
||
|
||
class ClassAsPAStarNew(tuple):
|
||
def __new__(cls, *args):
|
||
print("make a ClassAsPAStarNew", args)
|
||
return tuple.__new__(cls, *args[2].as_list())
|
||
|
||
def __str__(self):
|
||
return "".join(self)
|
||
|
||
A = pp.Literal("A").set_parse_action(pa0)
|
||
B = pp.Literal("B").set_parse_action(pa1)
|
||
C = pp.Literal("C").set_parse_action(pa2)
|
||
D = pp.Literal("D").set_parse_action(pa3)
|
||
E = pp.Literal("E").set_parse_action(Callable0())
|
||
F = pp.Literal("F").set_parse_action(Callable1())
|
||
G = pp.Literal("G").set_parse_action(Callable2())
|
||
H = pp.Literal("H").set_parse_action(Callable3())
|
||
I = pp.Literal("I").set_parse_action(CallableS0())
|
||
J = pp.Literal("J").set_parse_action(CallableS1())
|
||
K = pp.Literal("K").set_parse_action(CallableS2())
|
||
L = pp.Literal("L").set_parse_action(CallableS3())
|
||
M = pp.Literal("M").set_parse_action(CallableC0())
|
||
N = pp.Literal("N").set_parse_action(CallableC1())
|
||
O = pp.Literal("O").set_parse_action(CallableC2())
|
||
P = pp.Literal("P").set_parse_action(CallableC3())
|
||
Q = pp.Literal("Q").set_parse_action(paArgs)
|
||
R = pp.Literal("R").set_parse_action(parseActionHolder.pa3)
|
||
S = pp.Literal("S").set_parse_action(parseActionHolder.pa2)
|
||
T = pp.Literal("T").set_parse_action(parseActionHolder.pa1)
|
||
U = pp.Literal("U").set_parse_action(parseActionHolder.pa0)
|
||
V = pp.Literal("V")
|
||
|
||
# fmt: off
|
||
gg = pp.OneOrMore(
|
||
A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S | U | V | B | T
|
||
)
|
||
# fmt: on
|
||
testString = "VUTSRQPONMLKJIHGFEDCBA"
|
||
res = gg.parse_string(testString, parse_all=True)
|
||
print(res)
|
||
self.assertParseResultsEquals(
|
||
res,
|
||
expected_list=list(testString),
|
||
msg="Failed to parse using variable length parse actions",
|
||
)
|
||
|
||
A = pp.Literal("A").set_parse_action(ClassAsPA0)
|
||
B = pp.Literal("B").set_parse_action(ClassAsPA1)
|
||
C = pp.Literal("C").set_parse_action(ClassAsPA2)
|
||
D = pp.Literal("D").set_parse_action(ClassAsPA3)
|
||
E = pp.Literal("E").set_parse_action(ClassAsPAStarNew)
|
||
|
||
# fmt: off
|
||
gg = pp.OneOrMore(
|
||
A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S | T | U | V
|
||
)
|
||
# fmt: on
|
||
testString = "VUTSRQPONMLKJIHGFEDCBA"
|
||
res = gg.parse_string(testString, parse_all=True)
|
||
print(list(map(str, res)))
|
||
self.assertEqual(
|
||
list(testString),
|
||
list(map(str, res)),
|
||
"Failed to parse using variable length parse actions "
|
||
"using class constructors as parse actions",
|
||
)
|
||
|
||
def testSingleArgException(self):
|
||
testMessage = "just one arg"
|
||
try:
|
||
raise pp.ParseFatalException(testMessage)
|
||
except pp.ParseBaseException as pbe:
|
||
print("Received expected exception:", pbe)
|
||
raisedMsg = pbe.msg
|
||
self.assertEqual(
|
||
testMessage, raisedMsg, "Failed to get correct exception message"
|
||
)
|
||
|
||
def testOriginalTextFor(self):
|
||
def rfn(t):
|
||
return f"{t.src}:{len(''.join(t))}"
|
||
|
||
makeHTMLStartTag = lambda tag: pp.original_text_for(
|
||
pp.make_html_tags(tag)[0], as_string=False
|
||
)
|
||
|
||
# use the lambda, Luke
|
||
start = makeHTMLStartTag("IMG")
|
||
|
||
# don't replace our fancy parse action with rfn,
|
||
# append rfn to the list of parse actions
|
||
start.add_parse_action(rfn)
|
||
|
||
text = """_<img src="images/cal.png"
|
||
alt="cal image" width="16" height="15">_"""
|
||
s = start.transform_string(text)
|
||
print(s)
|
||
self.assertTrue(
|
||
s.startswith("_images/cal.png:"), "failed to preserve input s properly"
|
||
)
|
||
self.assertTrue(
|
||
s.endswith("77_"), "failed to return full original text properly"
|
||
)
|
||
|
||
tag_fields = makeHTMLStartTag("IMG").search_string(text)[0]
|
||
print(sorted(tag_fields.keys()))
|
||
self.assertEqual(
|
||
["alt", "empty", "height", "src", "startImg", "tag", "width"],
|
||
sorted(tag_fields.keys()),
|
||
"failed to preserve results names in original_text_for",
|
||
)
|
||
|
||
def testPackratParsingCacheCopy(self):
|
||
integer = pp.Word(pp.nums).set_name("integer")
|
||
id = pp.Word(pp.alphas + "_", pp.alphanums + "_")
|
||
simpleType = pp.Literal("int")
|
||
arrayType = simpleType + ("[" + pp.DelimitedList(integer) + "]")[...]
|
||
varType = arrayType | simpleType
|
||
varDec = varType + pp.DelimitedList(id + pp.Optional("=" + integer)) + ";"
|
||
|
||
codeBlock = pp.Literal("{}")
|
||
|
||
funcDef = (
|
||
pp.Optional(varType | "void")
|
||
+ id
|
||
+ "("
|
||
+ (pp.DelimitedList(varType + id) | "void" | pp.empty)
|
||
+ ")"
|
||
+ codeBlock
|
||
)
|
||
|
||
program = varDec | funcDef
|
||
input = "int f(){}"
|
||
self.assertParseAndCheckList(
|
||
program,
|
||
input,
|
||
["int", "f", "(", ")", "{}"],
|
||
msg="Error in packrat parsing",
|
||
verbose=True,
|
||
)
|
||
|
||
def testPackratParsingCacheCopyTest2(self):
|
||
DO, AA = list(map(pp.Keyword, "DO AA".split()))
|
||
LPAR, RPAR = list(map(pp.Suppress, "()"))
|
||
identifier = ~AA + pp.Word("Z")
|
||
|
||
function_name = identifier.copy()
|
||
# ~ function_name = ~AA + Word("Z") #identifier.copy()
|
||
expr = pp.Forward().set_name("expr")
|
||
expr <<= pp.Group(
|
||
function_name + LPAR + pp.Optional(pp.DelimitedList(expr)) + RPAR
|
||
).set_name("functionCall") | identifier.set_name(
|
||
"ident"
|
||
) # .set_debug()#.set_break()
|
||
|
||
stmt = DO + pp.Group(pp.DelimitedList(identifier + ".*" | expr))
|
||
result = stmt.parse_string("DO Z", parse_all=True)
|
||
print(result.as_list())
|
||
self.assertEqual(
|
||
1, len(result[1]), "packrat parsing is duplicating And term exprs"
|
||
)
|
||
|
||
def testParseResultsDel(self):
|
||
grammar = pp.OneOrMore(pp.Word(pp.nums))("ints") + pp.OneOrMore(
|
||
pp.Word(pp.alphas)
|
||
)("words")
|
||
res = grammar.parse_string("123 456 ABC DEF", parse_all=True)
|
||
print(res.dump())
|
||
origInts = res.ints.as_list()
|
||
origWords = res.words.as_list()
|
||
del res[1]
|
||
del res["words"]
|
||
print(res.dump())
|
||
self.assertEqual("ABC", res[1], "failed to delete 0'th element correctly")
|
||
self.assertEqual(
|
||
origInts,
|
||
res.ints.as_list(),
|
||
"updated named attributes, should have updated list only",
|
||
)
|
||
self.assertEqual("", res.words, "failed to update named attribute correctly")
|
||
self.assertEqual(
|
||
"DEF", res[-1], "updated list, should have updated named attributes only"
|
||
)
|
||
|
||
# delete a slice (modifies list, leaves dict intact)
|
||
del res[:-1]
|
||
self.assertEqual(res.as_list(), ["DEF"])
|
||
self.assertEqual(res.as_dict(), {"ints": ["123", "456"]})
|
||
|
||
def test_delitem_slices(self):
|
||
results = pp.Char(pp.alphas)("char*")[...].parse_string(pp.alphas[:10])
|
||
chars_list = list(pp.alphas[:10])
|
||
self.assertEqual(results.as_list(), chars_list)
|
||
print(results.char)
|
||
self.assertEqual(results.char.as_list(), chars_list)
|
||
|
||
del results[::2]
|
||
print(results.as_list())
|
||
self.assertEqual(results.as_list(), chars_list[1::2])
|
||
print(results.char)
|
||
self.assertEqual(results.char.as_list(), chars_list)
|
||
|
||
del results[:]
|
||
print(results.as_list())
|
||
self.assertEqual(results.as_list(), [])
|
||
self.assertEqual(results.char.as_list(), chars_list)
|
||
|
||
def test_delitem_slices_copy(self):
|
||
results = pp.Char(pp.alphas)("char*")[...].parse_string(pp.alphas[:10])
|
||
chars_list = list(pp.alphas[:10])
|
||
self.assertEqual(results.as_list(), chars_list)
|
||
print(results.char)
|
||
self.assertEqual(results.char.as_list(), chars_list)
|
||
|
||
res2 = results.copy()
|
||
|
||
del results[::2]
|
||
print(results.as_list())
|
||
self.assertEqual(results.as_list(), chars_list[1::2])
|
||
self.assertEqual(res2.as_list(), chars_list)
|
||
print(results.char)
|
||
self.assertEqual(results.char.as_list(), chars_list)
|
||
|
||
del results[:]
|
||
print(results.as_list())
|
||
self.assertEqual(results.as_list(), [])
|
||
self.assertEqual(res2.as_list(), chars_list)
|
||
self.assertEqual(results.char.as_list(), chars_list)
|
||
|
||
def testWithAttributeParseAction(self):
|
||
"""
|
||
This unit test checks with_attribute in these ways:
|
||
|
||
* Argument forms as keywords and tuples
|
||
* Selecting matching tags by attribute
|
||
* Case-insensitive attribute matching
|
||
* Correctly matching tags having the attribute, and rejecting tags not having the attribute
|
||
|
||
(Unit test written by voigts as part of the Google Highly Open Participation Contest)
|
||
"""
|
||
|
||
data = """
|
||
<a>1</a>
|
||
<a b="x">2</a>
|
||
<a B="x">3</a>
|
||
<a b="X">4</a>
|
||
<a b="y">5</a>
|
||
<a class="boo">8</ a>
|
||
"""
|
||
tagStart, tagEnd = pp.make_html_tags("a")
|
||
|
||
expr = tagStart + pp.Word(pp.nums)("value") + tagEnd
|
||
|
||
expected = (
|
||
[
|
||
["a", ["b", "x"], False, "2", "</a>"],
|
||
["a", ["b", "x"], False, "3", "</a>"],
|
||
],
|
||
[
|
||
["a", ["b", "x"], False, "2", "</a>"],
|
||
["a", ["b", "x"], False, "3", "</a>"],
|
||
],
|
||
[["a", ["class", "boo"], False, "8", "</a>"]],
|
||
)
|
||
|
||
for attrib, exp in zip(
|
||
[
|
||
pp.with_attribute(b="x"),
|
||
# with_attribute(B="x"),
|
||
pp.with_attribute(("b", "x")),
|
||
# with_attribute(("B", "x")),
|
||
pp.with_class("boo"),
|
||
],
|
||
expected,
|
||
):
|
||
tagStart.set_parse_action(attrib)
|
||
result = expr.search_string(data)
|
||
|
||
print(result.dump())
|
||
self.assertParseResultsEquals(
|
||
result,
|
||
expected_list=exp,
|
||
msg=f"Failed test, expected {expected}, got {result.as_list()}",
|
||
)
|
||
|
||
def testNestedExpressions(self):
|
||
"""
|
||
This unit test checks nested_expr in these ways:
|
||
- use of default arguments
|
||
- use of non-default arguments (such as a pyparsing-defined comment
|
||
expression in place of quoted_string)
|
||
- use of a custom content expression
|
||
- use of a pyparsing expression for opener and closer is *OPTIONAL*
|
||
- use of input data containing nesting delimiters
|
||
- correct grouping of parsed tokens according to nesting of opening
|
||
and closing delimiters in the input string
|
||
|
||
(Unit test written by christoph... as part of the Google Highly Open Participation Contest)
|
||
"""
|
||
|
||
# All defaults. Straight out of the example script. Also, qualifies for
|
||
# the bonus: note the fact that (Z | (E^F) & D) is not parsed :-).
|
||
# Tests for bug fixed in 1.4.10
|
||
print("Test defaults:")
|
||
teststring = "((ax + by)*C) (Z | (E^F) & D)"
|
||
|
||
expr = pp.nested_expr()
|
||
|
||
expected = [[["ax", "+", "by"], "*C"]]
|
||
result = expr.parse_string(teststring, parse_all=False)
|
||
print(result.dump())
|
||
self.assertParseResultsEquals(
|
||
result,
|
||
expected_list=expected,
|
||
msg=f"Defaults didn't work. That's a bad sign. Expected: {expected}, got: {result}",
|
||
)
|
||
|
||
# Going through non-defaults, one by one; trying to think of anything
|
||
# odd that might not be properly handled.
|
||
|
||
# Change opener
|
||
print("\nNon-default opener")
|
||
teststring = "[[ ax + by)*C)"
|
||
expected = [[["ax", "+", "by"], "*C"]]
|
||
expr = pp.nested_expr("[")
|
||
self.assertParseAndCheckList(
|
||
expr,
|
||
teststring,
|
||
expected,
|
||
f"Non-default opener didn't work. Expected: {expected}, got: {result}",
|
||
verbose=True,
|
||
)
|
||
|
||
# Change closer
|
||
print("\nNon-default closer")
|
||
|
||
teststring = "((ax + by]*C]"
|
||
expected = [[["ax", "+", "by"], "*C"]]
|
||
expr = pp.nested_expr(closer="]")
|
||
self.assertParseAndCheckList(
|
||
expr,
|
||
teststring,
|
||
expected,
|
||
f"Non-default closer didn't work. Expected: {expected}, got: {result}",
|
||
verbose=True,
|
||
)
|
||
|
||
# #Multicharacter opener, closer
|
||
# opener = "bar"
|
||
# closer = "baz"
|
||
print("\nLiteral expressions for opener and closer")
|
||
|
||
opener, closer = map(pp.Literal, "bar baz".split())
|
||
expr = pp.nested_expr(
|
||
opener, closer, content=pp.Regex(r"([^b ]|b(?!a)|ba(?![rz]))+")
|
||
)
|
||
|
||
teststring = "barbar ax + bybaz*Cbaz"
|
||
expected = [[["ax", "+", "by"], "*C"]]
|
||
self.assertParseAndCheckList(
|
||
expr,
|
||
teststring,
|
||
expected,
|
||
f"Multicharacter opener and closer didn't work. Expected: {expected}, got: {result}",
|
||
verbose=True,
|
||
)
|
||
|
||
# Lisp-ish comments
|
||
print("\nUse ignore expression (1)")
|
||
comment = pp.Regex(r";;.*")
|
||
teststring = """
|
||
(let ((greeting "Hello, world!")) ;;(foo bar
|
||
(display greeting))
|
||
"""
|
||
|
||
expected = [
|
||
[
|
||
"let",
|
||
[["greeting", '"Hello,', 'world!"']],
|
||
";;(foo bar",
|
||
["display", "greeting"],
|
||
]
|
||
]
|
||
expr = pp.nested_expr(ignore_expr=comment)
|
||
self.assertParseAndCheckList(
|
||
expr,
|
||
teststring,
|
||
expected,
|
||
f'Lisp-ish comments (";; <...> $") didn\'t work. Expected: {expected}, got: {result}',
|
||
verbose=True,
|
||
)
|
||
|
||
# Lisp-ish comments, using a standard bit of pyparsing, and an Or.
|
||
print("\nUse ignore expression (2)")
|
||
comment = ";;" + pp.rest_of_line
|
||
|
||
teststring = """
|
||
(let ((greeting "Hello, )world!")) ;;(foo bar
|
||
(display greeting))
|
||
"""
|
||
|
||
expected = [
|
||
[
|
||
"let",
|
||
[["greeting", '"Hello, )world!"']],
|
||
";;",
|
||
"(foo bar",
|
||
["display", "greeting"],
|
||
]
|
||
]
|
||
expr = pp.nested_expr(ignore_expr=(comment ^ pp.quoted_string))
|
||
self.assertParseAndCheckList(
|
||
expr,
|
||
teststring,
|
||
expected,
|
||
f'Lisp-ish comments (";; <...> $") and quoted strings didn\'t work. Expected: {expected}, got: {result}',
|
||
verbose=True,
|
||
)
|
||
|
||
def testNestedExpressions2(self):
|
||
"""test nested_expr with conditions that explore other paths
|
||
|
||
identical opener and closer
|
||
opener and/or closer of type other than string or iterable
|
||
multi-character opener and/or closer
|
||
single character opener and closer with ignore_expr=None
|
||
multi-character opener and/or closer with ignore_expr=None
|
||
"""
|
||
|
||
name = pp.Word(pp.alphanums + "_")
|
||
|
||
# identical opener and closer
|
||
with self.assertRaises(
|
||
ValueError, msg="matching opener and closer should raise error"
|
||
):
|
||
expr = name + pp.nested_expr(opener="{", closer="{")
|
||
|
||
# opener and/or closer of type other than string or iterable
|
||
with self.assertRaises(
|
||
ValueError, msg="opener and closer as ints should raise error"
|
||
):
|
||
expr = name + pp.nested_expr(opener=12, closer=18)
|
||
|
||
# multi-character opener and/or closer
|
||
tstMulti = "aName {{ outer {{ 'inner with opener {{ and closer }} in quoted string' }} }}"
|
||
expr = name + pp.nested_expr(opener="{{", closer="}}")
|
||
result = expr.parse_string(tstMulti, parse_all=True)
|
||
expected = [
|
||
"aName",
|
||
["outer", ["'inner with opener {{ and closer }} in quoted string'"]],
|
||
]
|
||
print(result.dump())
|
||
self.assertParseResultsEquals(
|
||
result, expected, msg="issue with multi-character opener and closer"
|
||
)
|
||
|
||
# single character opener and closer with ignore_expr=None
|
||
tst = "aName { outer { 'inner with opener { and closer } in quoted string' }}"
|
||
expr = name + pp.nested_expr(opener="{", closer="}", ignore_expr=None)
|
||
singleCharResult = expr.parse_string(tst, parse_all=True)
|
||
print(singleCharResult.dump())
|
||
|
||
# multi-character opener and/or closer with ignore_expr=None
|
||
expr = name + pp.nested_expr(opener="{{", closer="}}", ignore_expr=None)
|
||
multiCharResult = expr.parse_string(tstMulti, parse_all=True)
|
||
print(multiCharResult.dump())
|
||
|
||
self.assertParseResultsEquals(
|
||
singleCharResult,
|
||
multiCharResult.as_list(),
|
||
msg="using different openers and closers shouldn't affect resulting ParseResults",
|
||
)
|
||
|
||
def testNestedExpressions3(self):
|
||
|
||
prior_ws_chars = pp.ParserElement.DEFAULT_WHITE_CHARS
|
||
with ppt.reset_pyparsing_context():
|
||
pp.ParserElement.set_default_whitespace_chars("")
|
||
|
||
input_str = dedent(
|
||
"""\
|
||
selector
|
||
{
|
||
a:b;
|
||
c:d;
|
||
selector
|
||
{
|
||
a:b;
|
||
c:d;
|
||
}
|
||
y:z;
|
||
}"""
|
||
)
|
||
|
||
print(ppt.with_line_numbers(input_str, 1, 100))
|
||
|
||
nested_result = (
|
||
pp.nested_expr("{", "}").parse_string("{" + input_str + "}").as_list()
|
||
)
|
||
expected_result = [
|
||
[
|
||
"selector\n",
|
||
[
|
||
"\n a:b;\n c:d;\n selector\n ",
|
||
["\n a:b;\n c:d;\n "],
|
||
"\n y:z;\n",
|
||
],
|
||
]
|
||
]
|
||
self.assertEqual(nested_result, expected_result)
|
||
|
||
# make sure things have been put back properly
|
||
self.assertEqual(pp.ParserElement.DEFAULT_WHITE_CHARS, prior_ws_chars)
|
||
|
||
def testNestedExpressions4(self):
|
||
allowed = pp.alphas
|
||
plot_options_short = pp.nested_expr(
|
||
"[", "]", content=pp.OneOrMore(pp.Word(allowed) ^ pp.quoted_string)
|
||
).set_results_name("plot_options")
|
||
|
||
self.assertParseAndCheckList(
|
||
plot_options_short,
|
||
"[slkjdfl sldjf [lsdf'lsdf']]",
|
||
[["slkjdfl", "sldjf", ["lsdf", "'lsdf'"]]],
|
||
)
|
||
|
||
def testNestedExpressionDoesNotOverwriteParseActions(self):
|
||
content = pp.Word(pp.nums + " ")
|
||
|
||
content.add_parse_action(lambda t: None)
|
||
orig_pa = content.parseAction[0]
|
||
|
||
expr = pp.nested_expr(content=content)
|
||
assert content.parseAction[0] is orig_pa
|
||
|
||
def testNestedExpressionRandom(self):
|
||
import random
|
||
|
||
word_chars = pp.alphanums
|
||
|
||
def get_random_character(_charset=word_chars + " "):
|
||
return random.choice(_charset)
|
||
|
||
def create_random_quoted_string():
|
||
quote_char = random.choice(('"', "'"))
|
||
yield quote_char
|
||
yield from (get_random_character() for _ in range(random.randint(0, 12)))
|
||
yield quote_char
|
||
|
||
def create_random_nested_expression():
|
||
yield "["
|
||
|
||
if random.random() < 0.25:
|
||
yield from create_random_quoted_string()
|
||
|
||
for _ in range(random.randint(0, 16)):
|
||
rnd = random.random()
|
||
if rnd < 0.25:
|
||
yield from create_random_quoted_string()
|
||
elif rnd < 0.3:
|
||
yield from create_random_nested_expression()
|
||
else:
|
||
yield from (
|
||
get_random_character() for _ in range(random.randint(1, 4))
|
||
)
|
||
|
||
if random.random() < 0.25:
|
||
yield from create_random_quoted_string()
|
||
|
||
yield "]"
|
||
|
||
num_reps = 150
|
||
|
||
# simulate nested_expr
|
||
LBRACK, RBRACK = pp.Suppress.using_each("[]")
|
||
wd = pp.Word(word_chars)
|
||
qs = pp.quoted_string()
|
||
ls = pp.Forward()
|
||
ls <<= pp.Group(LBRACK + (qs | ls | wd)[...] + RBRACK)
|
||
|
||
def crack_nested_string(s) -> list:
|
||
return ls.parse_string(s, parse_all=True).as_list()
|
||
|
||
expr = pp.nested_expr("[", "]")
|
||
for _ in range(num_reps):
|
||
nested_str = "".join(create_random_nested_expression())
|
||
# print(nested_str)
|
||
cracked_result = crack_nested_string(nested_str)
|
||
self.assertParseAndCheckList(
|
||
expr,
|
||
nested_str,
|
||
cracked_result,
|
||
f"Failed: {nested_str}, expected {cracked_result}",
|
||
verbose=False,
|
||
)
|
||
|
||
# test multi-character nesting delimiters
|
||
expr = pp.nested_expr("<<", ">>")
|
||
for _ in range(num_reps):
|
||
nested_str = "".join(create_random_nested_expression())
|
||
# print(nested_str)
|
||
cracked_result = crack_nested_string(nested_str)
|
||
nested_str = nested_str.replace("[", "<<").replace("]", ">>")
|
||
self.assertParseAndCheckList(
|
||
expr,
|
||
nested_str,
|
||
cracked_result,
|
||
f"Failed: {nested_str}, expected {cracked_result}",
|
||
verbose=False,
|
||
)
|
||
|
||
# test with no ignore_expr (no quoted string handling)
|
||
expr = pp.nested_expr("[", "]", ignore_expr=None)
|
||
for _ in range(num_reps):
|
||
nested_str = "".join(create_random_nested_expression())
|
||
nested_str = nested_str.replace('"', "").replace("'", "")
|
||
# print(nested_str)
|
||
cracked_result = crack_nested_string(nested_str)
|
||
self.assertParseAndCheckList(
|
||
expr,
|
||
nested_str,
|
||
cracked_result,
|
||
f"Failed: {nested_str}, expected {cracked_result}",
|
||
verbose=False,
|
||
)
|
||
|
||
# test multi-character nesting delimiters, with no ignore_expr
|
||
expr = pp.nested_expr("<<", ">>", ignore_expr=None)
|
||
for _ in range(num_reps):
|
||
nested_str = "".join(create_random_nested_expression())
|
||
nested_str = nested_str.replace('"', "").replace("'", "")
|
||
# print(nested_str)
|
||
cracked_result = crack_nested_string(nested_str)
|
||
nested_str = nested_str.replace("[", "<<").replace("]", ">>")
|
||
self.assertParseAndCheckList(
|
||
expr,
|
||
nested_str,
|
||
cracked_result,
|
||
f"Failed: {nested_str}, expected {cracked_result}",
|
||
verbose=False,
|
||
)
|
||
|
||
def testWordMinMaxArgs(self):
|
||
parsers = [
|
||
"A" + pp.Word(pp.nums),
|
||
"A" + pp.Word(pp.nums, min=1),
|
||
"A" + pp.Word(pp.nums, max=6),
|
||
"A" + pp.Word(pp.nums, min=1, max=6),
|
||
"A" + pp.Word(pp.nums, min=1),
|
||
"A" + pp.Word(pp.nums, min=2),
|
||
"A" + pp.Word(pp.nums, min=2, max=6),
|
||
pp.Word("A", pp.nums),
|
||
pp.Word("A", pp.nums, min=1),
|
||
pp.Word("A", pp.nums, max=6),
|
||
pp.Word("A", pp.nums, min=1, max=6),
|
||
pp.Word("A", pp.nums, min=1),
|
||
pp.Word("A", pp.nums, min=2),
|
||
pp.Word("A", pp.nums, min=2, max=6),
|
||
pp.Word(pp.alphas, pp.nums),
|
||
pp.Word(pp.alphas, pp.nums, min=1),
|
||
pp.Word(pp.alphas, pp.nums, max=6),
|
||
pp.Word(pp.alphas, pp.nums, min=1, max=6),
|
||
pp.Word(pp.alphas, pp.nums, min=1),
|
||
pp.Word(pp.alphas, pp.nums, min=2),
|
||
pp.Word(pp.alphas, pp.nums, min=2, max=6),
|
||
]
|
||
|
||
fails = []
|
||
for p in parsers:
|
||
print(p, getattr(p, "reString", "..."), end=" ", flush=True)
|
||
try:
|
||
p.parse_string("A123", parse_all=True)
|
||
except Exception as e:
|
||
print(" <<< FAIL")
|
||
fails.append(p)
|
||
else:
|
||
print()
|
||
if fails:
|
||
self.fail(f"{','.join(str(f) for f in fails)} failed to match")
|
||
|
||
def testWordMinMaxExactArgs(self):
|
||
for minarg in range(1, 9):
|
||
for maxarg in range(minarg, 10):
|
||
with self.subTest(minarg=minarg, maxarg=maxarg):
|
||
expr = pp.Word("AB", pp.nums, min=minarg, max=maxarg)
|
||
print(minarg, maxarg, expr.reString, end=" ")
|
||
trailing = expr.reString.rpartition("]")[-1]
|
||
expected_special = {
|
||
(1, 1): "",
|
||
(1, 2): "?",
|
||
(2, 2): "",
|
||
}
|
||
expected_default = (
|
||
f"{{{minarg - 1}}}"
|
||
if minarg == maxarg
|
||
else f"{{{minarg - 1},{maxarg - 1}}}"
|
||
)
|
||
expected = expected_special.get((minarg, maxarg), expected_default)
|
||
|
||
print(trailing == expected)
|
||
|
||
self.assertEqual(trailing, expected)
|
||
|
||
self.assertParseAndCheckList(
|
||
expr + pp.rest_of_line.suppress(),
|
||
"A1234567890",
|
||
["A1234567890"[:maxarg]],
|
||
)
|
||
|
||
for exarg in range(1, 9):
|
||
with self.subTest(exarg=exarg):
|
||
expr = pp.Word("AB", pp.nums, exact=exarg)
|
||
print(exarg, expr.reString, end=" ")
|
||
trailing = expr.reString.rpartition("]")[-1]
|
||
if exarg < 3:
|
||
expected = ""
|
||
else:
|
||
expected = f"{{{exarg - 1}}}"
|
||
print(trailing == expected)
|
||
|
||
self.assertEqual(trailing, expected)
|
||
|
||
self.assertParseAndCheckList(
|
||
expr + pp.rest_of_line.suppress(),
|
||
"A1234567890",
|
||
["A1234567890"[:exarg]],
|
||
)
|
||
|
||
def testWordMaxIndependentOfWhitespaceInCharset(self):
|
||
# A Word constructed with an explicit `max` uses an internal regex
|
||
# fast-path only when its character set contains no space; otherwise
|
||
# it falls back to the char-by-char parseImpl. Both paths must agree:
|
||
# `max` means "match at most `max` characters, leaving the remainder"
|
||
# (see testWordMinMaxExactArgs and the "W:(0-9){1,3}" repr contract).
|
||
for extra, path in [("", "regex fast-path"), (" ", "char-by-char path")]:
|
||
with self.subTest(charset_extra=repr(extra), path=path):
|
||
expr = pp.Word(pp.nums + extra, max=3)("field") + pp.Word(pp.nums)(
|
||
"rest"
|
||
)
|
||
self.assertParseAndCheckDict(
|
||
expr,
|
||
"0123456",
|
||
{"field": "012", "rest": "3456"},
|
||
"Failed to parse regex and non-regex parse_impl in Word consistently"
|
||
)
|
||
|
||
def testWordMin(self):
|
||
# failing tests
|
||
for min_val in range(3, 5):
|
||
with self.subTest(min_val=min_val):
|
||
wd = pp.Word("a", "1", min=min_val)
|
||
print(min_val, wd.reString)
|
||
with self.assertRaisesParseException():
|
||
wd.parse_string("a1")
|
||
|
||
for min_val in range(2, 5):
|
||
with self.subTest(min_val=min_val):
|
||
wd = pp.Word("a", min=min_val)
|
||
print(min_val, wd.reString)
|
||
with self.assertRaisesParseException():
|
||
wd.parse_string("a")
|
||
|
||
for min_val in range(3, 5):
|
||
with self.subTest(min_val=min_val):
|
||
wd = pp.Word("a", "1", min=min_val)
|
||
print(min_val, wd.reString)
|
||
with self.assertRaisesParseException():
|
||
wd.parse_string("a1")
|
||
|
||
# passing tests
|
||
for min_val in range(2, 5):
|
||
with self.subTest(min_val=min_val):
|
||
wd = pp.Word("a", min=min_val)
|
||
test_string = "a" * min_val
|
||
self.assertParseAndCheckList(
|
||
wd,
|
||
test_string,
|
||
[test_string],
|
||
msg=f"Word(min={min_val}) failed",
|
||
verbose=True,
|
||
)
|
||
|
||
for min_val in range(2, 5):
|
||
with self.subTest(min_val=min_val):
|
||
wd = pp.Word("a", "1", min=min_val)
|
||
test_string = "a" + "1" * (min_val - 1)
|
||
self.assertParseAndCheckList(
|
||
wd,
|
||
test_string,
|
||
[test_string],
|
||
msg=f"Word(min={min_val}) failed",
|
||
verbose=True,
|
||
)
|
||
|
||
def testWordExact(self):
|
||
# failing tests
|
||
for exact_val in range(2, 5):
|
||
with self.subTest(exact_val=exact_val):
|
||
wd = pp.Word("a", exact=exact_val)
|
||
print(exact_val, wd.reString)
|
||
with self.assertRaisesParseException():
|
||
wd.parse_string("a")
|
||
|
||
# passing tests
|
||
for exact_val in range(2, 5):
|
||
with self.subTest(exact_val=exact_val):
|
||
wd = pp.Word("a", exact=exact_val)
|
||
test_string = "a" * exact_val
|
||
self.assertParseAndCheckList(
|
||
wd,
|
||
test_string,
|
||
[test_string],
|
||
msg=f"Word(exact={exact_val}) failed",
|
||
verbose=True,
|
||
)
|
||
|
||
def testInvalidMinMaxArgs(self):
|
||
with self.assertRaises(ValueError):
|
||
wd = pp.Word(min=2, max=1)
|
||
|
||
def testWordExclude(self):
|
||
allButPunc = pp.Word(pp.printables, exclude_chars=".,:;-_!?")
|
||
|
||
test = "Hello, Mr. Ed, it's Wilbur!"
|
||
result = allButPunc.search_string(test).as_list()
|
||
print(result)
|
||
self.assertEqual(
|
||
[["Hello"], ["Mr"], ["Ed"], ["it's"], ["Wilbur"]],
|
||
result,
|
||
"failed WordExcludeTest",
|
||
)
|
||
|
||
def testWordExclude2(self):
|
||
punc_chars = ".,:;-_!?"
|
||
|
||
all_but_punc = pp.Word(pp.printables, exclude_chars=punc_chars)
|
||
all_and_punc = pp.Word(pp.printables)
|
||
|
||
assert set(punc_chars) & set(all_but_punc.init_chars) == set()
|
||
|
||
expr = all_but_punc("no_punc*") | all_and_punc("with_punc*")
|
||
|
||
self.assertParseAndCheckDict(
|
||
expr[...],
|
||
"Mr. Ed,",
|
||
{"no_punc": ["Mr", "Ed"], "with_punc": [".", ","]},
|
||
"failed matching with exclude_chars (1)",
|
||
)
|
||
|
||
self.assertParseAndCheckDict(
|
||
expr[...],
|
||
":Mr. Ed,",
|
||
{"no_punc": ["Ed"], "with_punc": [":Mr.", ","]},
|
||
"failed matching with exclude_chars (2)",
|
||
)
|
||
|
||
def testWordMinOfZero(self):
|
||
"""test a Word with min=0"""
|
||
|
||
with self.assertRaises(ValueError, msg="expected min 0 to error"):
|
||
expr = pp.Word(pp.nums, min=0, max=10)
|
||
|
||
@staticmethod
|
||
def setup_testWordMaxGreaterThanZeroAndAsKeyword():
|
||
# fmt: off
|
||
bool_operand = (
|
||
pp.Word(pp.alphas, max=1, as_keyword=True)
|
||
| pp.one_of("True False")
|
||
)
|
||
test_string = "p q r False"
|
||
return SimpleNamespace(**locals())
|
||
# fmt: on
|
||
|
||
def testWordMaxGreaterThanZeroAndAsKeyword1(self):
|
||
"""test a Word with max>0 and as_keyword=True"""
|
||
setup = self.setup_testWordMaxGreaterThanZeroAndAsKeyword()
|
||
|
||
result = setup.bool_operand[...].parse_string(setup.test_string, parse_all=True)
|
||
self.assertParseAndCheckList(
|
||
setup.bool_operand[...],
|
||
setup.test_string,
|
||
setup.test_string.split(),
|
||
msg=f"{__()}Failed to parse Word(max=1, as_keyword=True)",
|
||
verbose=True,
|
||
)
|
||
|
||
def testWordMaxGreaterThanZeroAndAsKeyword2(self):
|
||
"""test a Word with max>0 and as_keyword=True"""
|
||
setup = self.setup_testWordMaxGreaterThanZeroAndAsKeyword()
|
||
|
||
with self.assertRaisesParseException(
|
||
msg=f"{__()}Failed to detect Word with max > 0 and as_keyword=True"
|
||
):
|
||
setup.bool_operand.parse_string("abc", parse_all=True)
|
||
|
||
def testCharAsKeyword(self):
|
||
"""test a Char with as_keyword=True"""
|
||
|
||
grade = pp.OneOrMore(pp.Char("ABCDF", as_keyword=True))
|
||
|
||
# all single char words
|
||
result = grade.parse_string("B B C A D", parse_all=True)
|
||
|
||
print(result)
|
||
expected = ["B", "B", "C", "A", "D"]
|
||
self.assertParseResultsEquals(
|
||
result, expected, msg="issue with Char as_keyword=True"
|
||
)
|
||
|
||
# NOT all single char words
|
||
test2 = "B BB C A D"
|
||
result2 = grade.parse_string(test2, parse_all=False)
|
||
|
||
print(result2)
|
||
expected2 = ["B"]
|
||
self.assertParseResultsEquals(
|
||
result2, expected2, msg="issue with Char as_keyword=True parsing 2 chars"
|
||
)
|
||
|
||
def testCharRe(self):
|
||
expr = pp.Char("ABCDEFG")
|
||
self.assertEqual("[A-G]", expr.reString)
|
||
|
||
def testCharsNotIn(self):
|
||
"""test CharsNotIn initialized with various arguments"""
|
||
|
||
vowels = "AEIOU"
|
||
tst = "bcdfghjklmnpqrstvwxyz"
|
||
|
||
# default args
|
||
consonants = pp.CharsNotIn(vowels)
|
||
result = consonants.parse_string(tst, parse_all=True)
|
||
print(result)
|
||
self.assertParseResultsEquals(
|
||
result, [tst], msg="issue with CharsNotIn w/ default args"
|
||
)
|
||
|
||
# min = 0
|
||
with self.assertRaises(ValueError, msg="issue with CharsNotIn w/ min=0"):
|
||
consonants = pp.CharsNotIn(vowels, min=0)
|
||
|
||
# max > 0
|
||
consonants = pp.CharsNotIn(vowels, max=5)
|
||
result = consonants.parse_string(tst, parse_all=False)
|
||
print(result)
|
||
self.assertParseResultsEquals(
|
||
result, [tst[:5]], msg="issue with CharsNotIn w max > 0"
|
||
)
|
||
|
||
# exact > 0
|
||
consonants = pp.CharsNotIn(vowels, exact=10)
|
||
result = consonants.parse_string(tst[:10], parse_all=True)
|
||
print(result)
|
||
self.assertParseResultsEquals(
|
||
result, [tst[:10]], msg="issue with CharsNotIn w/ exact > 0"
|
||
)
|
||
|
||
# min > length
|
||
consonants = pp.CharsNotIn(vowels, min=25)
|
||
with self.assertRaisesParseException(msg="issue with CharsNotIn min > tokens"):
|
||
result = consonants.parse_string(tst, parse_all=True)
|
||
|
||
def testParseAll(self):
|
||
testExpr = pp.Word("A")
|
||
|
||
tests = [
|
||
("AAAAA", False, True),
|
||
("AAAAA", True, True),
|
||
("AAABB", False, True),
|
||
("AAABB", True, False),
|
||
]
|
||
for s, parse_allFlag, shouldSucceed in tests:
|
||
try:
|
||
print(
|
||
f"'{s}' parse_all={parse_allFlag} (shouldSucceed={shouldSucceed})"
|
||
)
|
||
testExpr.parse_string(s, parse_all=parse_allFlag)
|
||
self.assertTrue(
|
||
shouldSucceed, "successfully parsed when should have failed"
|
||
)
|
||
except ParseException as pe:
|
||
print(pe.explain())
|
||
self.assertFalse(
|
||
shouldSucceed, "failed to parse when should have succeeded"
|
||
)
|
||
|
||
# add test for trailing comments
|
||
testExpr.ignore(pp.cpp_style_comment)
|
||
|
||
tests = [
|
||
("AAAAA //blah", False, True),
|
||
("AAAAA //blah", True, True),
|
||
("AAABB //blah", False, True),
|
||
("AAABB //blah", True, False),
|
||
]
|
||
for s, parse_allFlag, shouldSucceed in tests:
|
||
try:
|
||
print(
|
||
f"'{s}' parse_all={parse_allFlag} (shouldSucceed={shouldSucceed})"
|
||
)
|
||
testExpr.parse_string(s, parse_all=parse_allFlag)
|
||
self.assertTrue(
|
||
shouldSucceed, "successfully parsed when should have failed"
|
||
)
|
||
except ParseException as pe:
|
||
print(pe.explain())
|
||
self.assertFalse(
|
||
shouldSucceed, "failed to parse when should have succeeded"
|
||
)
|
||
|
||
# add test with very long expression string
|
||
# testExpr = pp.MatchFirst([pp.Literal(c) for c in pp.printables if c != 'B'])[1, ...]
|
||
anything_but_an_f = pp.OneOrMore(
|
||
pp.MatchFirst([pp.Literal(c) for c in pp.printables if c != "f"])
|
||
)
|
||
testExpr = pp.Word("012") + anything_but_an_f
|
||
|
||
tests = [
|
||
("00aab", False, True),
|
||
("00aab", True, True),
|
||
("00aaf", False, True),
|
||
("00aaf", True, False),
|
||
]
|
||
for s, parse_allFlag, shouldSucceed in tests:
|
||
try:
|
||
print(
|
||
f"'{s}' parse_all={parse_allFlag} (shouldSucceed={shouldSucceed})"
|
||
)
|
||
testExpr.parse_string(s, parse_all=parse_allFlag)
|
||
self.assertTrue(
|
||
shouldSucceed, "successfully parsed when should have failed"
|
||
)
|
||
except ParseException as pe:
|
||
print(pe.explain())
|
||
self.assertFalse(
|
||
shouldSucceed, "failed to parse when should have succeeded"
|
||
)
|
||
|
||
def testGreedyQuotedStrings(self):
|
||
src = """\
|
||
"string1", "strin""g2"
|
||
'string1', 'string2'
|
||
^string1^, ^string2^
|
||
<string1>, <string2>"""
|
||
|
||
testExprs = (
|
||
pp.sgl_quoted_string,
|
||
pp.dbl_quoted_string,
|
||
pp.quoted_string,
|
||
pp.QuotedString('"', esc_quote='""'),
|
||
pp.QuotedString("'", esc_quote="''"),
|
||
pp.QuotedString("^"),
|
||
pp.QuotedString("<", end_quote_char=">"),
|
||
)
|
||
for expr in testExprs:
|
||
strs = pp.DelimitedList(expr).search_string(src)
|
||
print(strs)
|
||
self.assertTrue(
|
||
bool(strs), f"no matches found for test expression '{expr}'"
|
||
)
|
||
for lst in strs:
|
||
self.assertEqual(
|
||
2, len(lst), f"invalid match found for test expression '{expr}'"
|
||
)
|
||
|
||
src = """'ms1',1,0,'2009-12-22','2009-12-22 10:41:22') ON DUPLICATE KEY UPDATE sent_count = sent_count + 1, mtime = '2009-12-22 10:41:22';"""
|
||
tok_sql_quoted_value = pp.QuotedString(
|
||
"'", "\\", "''", True, False
|
||
) ^ pp.QuotedString('"', "\\", '""', True, False)
|
||
tok_sql_computed_value = pp.Word(pp.nums)
|
||
tok_sql_identifier = pp.Word(pp.alphas)
|
||
|
||
val = tok_sql_quoted_value | tok_sql_computed_value | tok_sql_identifier
|
||
vals = pp.DelimitedList(val)
|
||
print(vals.parse_string(src, parse_all=False))
|
||
self.assertEqual(
|
||
5,
|
||
len(vals.parse_string(src, parse_all=False)),
|
||
"error in greedy quote escaping",
|
||
)
|
||
|
||
def testQuotedStringEscapedQuotes(self):
|
||
quoted = pp.QuotedString('"', esc_quote='""')
|
||
res = quoted.parse_string('"like ""SQL"""', parse_all=True)
|
||
print(res.as_list())
|
||
self.assertEqual(['like "SQL"'], res.as_list())
|
||
|
||
# Issue #263 - handle case when the esc_quote is not a repeated character
|
||
quoted = pp.QuotedString("y", esc_char=None, esc_quote="xy")
|
||
res = quoted.parse_string("yaaay", parse_all=True)
|
||
self.assertEqual(["aaa"], res.as_list())
|
||
res = quoted.parse_string("yaaaxyaaay", parse_all=True)
|
||
print(res.as_list())
|
||
self.assertEqual(["aaayaaa"], res.as_list())
|
||
|
||
def testQuotedStringEscapedExtendedChars(self):
|
||
quoted = pp.QuotedString("'")
|
||
self.assertParseAndCheckList(
|
||
quoted,
|
||
"'null: \0 octal: \267 hex: \xb7 unicode: \u00b7'",
|
||
["null: \x00 octal: · hex: · unicode: ·"],
|
||
"failed to parse embedded numeric escapes",
|
||
)
|
||
|
||
def testWordBoundaryExpressions(self):
|
||
ws = pp.WordStart()
|
||
we = pp.WordEnd()
|
||
vowel = pp.one_of(list("AEIOUY"))
|
||
consonant = pp.one_of(list("BCDFGHJKLMNPQRSTVWXZ"))
|
||
|
||
leadingVowel = ws + vowel
|
||
trailingVowel = vowel + we
|
||
leadingConsonant = ws + consonant
|
||
trailingConsonant = consonant + we
|
||
internalVowel = ~ws + vowel + ~we
|
||
|
||
bnf = leadingVowel | trailingVowel
|
||
|
||
tests = """\
|
||
ABC DEF GHI
|
||
JKL MNO PQR
|
||
STU VWX YZ """.splitlines()
|
||
tests.append("\n".join(tests))
|
||
|
||
expectedResult = [
|
||
[["D", "G"], ["A"], ["C", "F"], ["I"], ["E"], ["A", "I"]],
|
||
[["J", "M", "P"], [], ["L", "R"], ["O"], [], ["O"]],
|
||
[["S", "V"], ["Y"], ["X", "Z"], ["U"], [], ["U", "Y"]],
|
||
[
|
||
["D", "G", "J", "M", "P", "S", "V"],
|
||
["A", "Y"],
|
||
["C", "F", "L", "R", "X", "Z"],
|
||
["I", "O", "U"],
|
||
["E"],
|
||
["A", "I", "O", "U", "Y"],
|
||
],
|
||
]
|
||
|
||
for t, expected in zip(tests, expectedResult):
|
||
print(t)
|
||
results = [
|
||
flatten(e.search_string(t).as_list())
|
||
for e in [
|
||
leadingConsonant,
|
||
leadingVowel,
|
||
trailingConsonant,
|
||
trailingVowel,
|
||
internalVowel,
|
||
bnf,
|
||
]
|
||
]
|
||
print(results)
|
||
print()
|
||
self.assertEqual(
|
||
expected,
|
||
results,
|
||
f"Failed WordBoundaryTest, expected {expected}, got {results}",
|
||
)
|
||
|
||
def testWordBoundaryExpressions2(self):
|
||
from itertools import product
|
||
|
||
ws1 = pp.WordStart(pp.alphas)
|
||
ws2 = pp.WordStart(word_chars=pp.alphas)
|
||
ws3 = pp.WordStart(word_chars=pp.alphas)
|
||
we1 = pp.WordEnd(pp.alphas)
|
||
we2 = pp.WordEnd(word_chars=pp.alphas)
|
||
we3 = pp.WordEnd(word_chars=pp.alphas)
|
||
|
||
for i, (ws, we) in enumerate(product((ws1, ws2, ws3), (we1, we2, we3))):
|
||
try:
|
||
expr = "(" + ws + pp.Word(pp.alphas) + we + ")"
|
||
expr.parse_string("(abc)", parse_all=True)
|
||
except pp.ParseException as pe:
|
||
self.fail(f"Test {i} failed: {pe}")
|
||
else:
|
||
pass
|
||
|
||
def testRequiredEach(self):
|
||
parser = pp.Keyword("bam") & pp.Keyword("boo")
|
||
try:
|
||
res1 = parser.parse_string("bam boo", parse_all=True)
|
||
print(res1.as_list())
|
||
res2 = parser.parse_string("boo bam", parse_all=True)
|
||
print(res2.as_list())
|
||
except ParseException:
|
||
failed = True
|
||
else:
|
||
failed = False
|
||
self.assertFalse(failed, "invalid logic in Each")
|
||
|
||
self.assertEqual(
|
||
set(res1),
|
||
set(res2),
|
||
f"Failed RequiredEachTest, expected {res1.as_list()}"
|
||
f" and {res2.as_list} to contain the same words in any order",
|
||
)
|
||
|
||
def testOptionalEachTest1(self):
|
||
for the_input in [
|
||
"Tal Weiss Major",
|
||
"Tal Major",
|
||
"Weiss Major",
|
||
"Major",
|
||
"Major Tal",
|
||
"Major Weiss",
|
||
"Major Tal Weiss",
|
||
]:
|
||
print(the_input)
|
||
parser1 = (pp.Optional("Tal") + pp.Optional("Weiss")) & pp.Keyword("Major")
|
||
parser2 = pp.Optional(
|
||
pp.Optional("Tal") + pp.Optional("Weiss")
|
||
) & pp.Keyword("Major")
|
||
parser3 = (pp.Keyword("Tal") | pp.Keyword("Weiss"))[...] & pp.Keyword(
|
||
"Major"
|
||
)
|
||
|
||
p1res = parser1.parse_string(the_input, parse_all=True)
|
||
|
||
p2res = parser2.parse_string(the_input, parse_all=True)
|
||
self.assertEqual(
|
||
p1res.as_list(),
|
||
p2res.as_list(),
|
||
f"Each failed to match with nested Optionals, {p1res.as_list()} should match {p2res.as_list()}",
|
||
)
|
||
|
||
p3res = parser3.parse_string(the_input, parse_all=True)
|
||
self.assertEqual(
|
||
p1res.as_list(),
|
||
p3res.as_list(),
|
||
f"Each failed to match with repeated Optionals, {p1res.as_list()} should match {p3res.as_list()}",
|
||
)
|
||
|
||
def testOptionalEachTest2(self):
|
||
word = pp.Word(pp.alphanums + "_").set_name("word")
|
||
with_stmt = "with" + pp.OneOrMore(pp.Group(word("key") + "=" + word("value")))(
|
||
"overrides"
|
||
)
|
||
using_stmt = "using" + pp.Regex("id-[0-9a-f]{8}")("id")
|
||
modifiers = pp.Optional(with_stmt("with_stmt")) & pp.Optional(
|
||
using_stmt("using_stmt")
|
||
)
|
||
|
||
self.assertEqual("with foo=bar bing=baz using id-deadbeef", modifiers)
|
||
self.assertNotEqual(
|
||
"with foo=bar bing=baz using id-deadbeef using id-feedfeed", modifiers
|
||
)
|
||
|
||
def testOptionalEachTest3(self):
|
||
foo = pp.Literal("foo")
|
||
bar = pp.Literal("bar")
|
||
|
||
openBrace = pp.Suppress(pp.Literal("{"))
|
||
closeBrace = pp.Suppress(pp.Literal("}"))
|
||
|
||
exp = openBrace + (foo[1, ...]("foo") & bar[...]("bar")) + closeBrace
|
||
|
||
tests = """\
|
||
{foo}
|
||
{bar foo bar foo bar foo}
|
||
""".splitlines()
|
||
for test in tests:
|
||
test = test.strip()
|
||
if not test:
|
||
continue
|
||
self.assertParseAndCheckList(
|
||
exp,
|
||
test,
|
||
test.strip("{}").split(),
|
||
f"failed to parse Each expression {test!r}",
|
||
verbose=True,
|
||
)
|
||
|
||
with self.assertRaisesParseException():
|
||
exp.parse_string("{bar}", parse_all=True)
|
||
|
||
def testOptionalEachTest4(self):
|
||
expr = (~ppc.iso8601_date + ppc.integer("id")) & (
|
||
pp.Group(ppc.iso8601_date)("date*")[...]
|
||
)
|
||
|
||
success, _ = expr.run_tests(
|
||
"""
|
||
1999-12-31 100 2001-01-01
|
||
42
|
||
"""
|
||
)
|
||
self.assertTrue(success)
|
||
|
||
def testEachWithParseFatalException(self):
|
||
option_expr = pp.Keyword("options") - "(" + ppc.integer + ")"
|
||
step_expr1 = pp.Keyword("step") - "(" + ppc.integer + ")"
|
||
step_expr2 = pp.Keyword("step") - "(" + ppc.integer + "Z" + ")"
|
||
step_expr = step_expr1 ^ step_expr2
|
||
|
||
parser = option_expr & step_expr[...]
|
||
tests = [
|
||
(
|
||
"options(100) step(A)",
|
||
"Expected integer, found 'A' (at char 18), (line:1, col:19)",
|
||
),
|
||
(
|
||
"step(A) options(100)",
|
||
"Expected integer, found 'A' (at char 5), (line:1, col:6)",
|
||
),
|
||
(
|
||
"options(100) step(100A)",
|
||
"""Expected 'Z', found 'A' (at char 21), (line:1, col:22)""",
|
||
),
|
||
(
|
||
"options(100) step(22) step(100ZA)",
|
||
"""Expected ')', found 'A' (at char 31), (line:1, col:32)""",
|
||
),
|
||
]
|
||
test_lookup = dict(tests)
|
||
|
||
success, output = parser.run_tests((t[0] for t in tests), failure_tests=True)
|
||
for test_str, result in output:
|
||
self.assertEqual(
|
||
test_lookup[test_str],
|
||
str(result),
|
||
f"incorrect exception raised for test string {test_str!r}",
|
||
)
|
||
|
||
def testEachWithMultipleMatch(self):
|
||
size = "size" + pp.one_of("S M L XL")
|
||
color = pp.Group(
|
||
"color" + pp.one_of("red orange yellow green blue purple white black brown")
|
||
)
|
||
size.set_name("size_spec")
|
||
color.set_name("color_spec")
|
||
|
||
spec0 = size("size") & color[...]("colors")
|
||
spec1 = size("size") & color[1, ...]("colors")
|
||
|
||
for spec in (spec0, spec1):
|
||
for test, expected_dict in [
|
||
(
|
||
"size M color red color yellow",
|
||
{
|
||
"colors": [["color", "red"], ["color", "yellow"]],
|
||
"size": ["size", "M"],
|
||
},
|
||
),
|
||
(
|
||
"color green size M color red color yellow",
|
||
{
|
||
"colors": [
|
||
["color", "green"],
|
||
["color", "red"],
|
||
["color", "yellow"],
|
||
],
|
||
"size": ["size", "M"],
|
||
},
|
||
),
|
||
]:
|
||
result = spec.parse_string(test, parse_all=True)
|
||
self.assertParseResultsEquals(result, expected_dict=expected_dict)
|
||
|
||
def testSumParseResults(self):
|
||
samplestr1 = "garbage;DOB 10-10-2010;more garbage\nID PARI12345678;more garbage"
|
||
samplestr2 = "garbage;ID PARI12345678;more garbage\nDOB 10-10-2010;more garbage"
|
||
samplestr3 = "garbage;DOB 10-10-2010"
|
||
samplestr4 = "garbage;ID PARI12345678;more garbage- I am cool"
|
||
|
||
res1 = "ID:PARI12345678 DOB:10-10-2010 INFO:"
|
||
res2 = "ID:PARI12345678 DOB:10-10-2010 INFO:"
|
||
res3 = "ID: DOB:10-10-2010 INFO:"
|
||
res4 = "ID:PARI12345678 DOB: INFO: I am cool"
|
||
|
||
dob_ref = "DOB" + pp.Regex(r"\d{2}-\d{2}-\d{4}")("dob")
|
||
id_ref = "ID" + pp.Word(pp.alphanums, exact=12)("id")
|
||
info_ref = "-" + pp.rest_of_line("info")
|
||
|
||
person_data = dob_ref | id_ref | info_ref
|
||
|
||
tests = (samplestr1, samplestr2, samplestr3, samplestr4)
|
||
results = (res1, res2, res3, res4)
|
||
for test, expected in zip(tests, results):
|
||
person = sum(person_data.search_string(test))
|
||
result = f"ID:{person.id} DOB:{person.dob} INFO:{person.info}"
|
||
print(test)
|
||
print(expected)
|
||
print(result)
|
||
for pd in person_data.search_string(test):
|
||
print(pd.dump())
|
||
print()
|
||
self.assertEqual(
|
||
expected,
|
||
result,
|
||
f"Failed to parse '{test}' correctly, \nexpected '{expected}', got '{result}'",
|
||
)
|
||
|
||
def testMarkInputLine(self):
|
||
samplestr1 = "DOB 100-10-2010;more garbage\nID PARI12345678;more garbage"
|
||
|
||
dob_ref = "DOB" + pp.Regex(r"\d{2}-\d{2}-\d{4}")("dob")
|
||
|
||
try:
|
||
res = dob_ref.parse_string(samplestr1, parse_all=True)
|
||
except ParseException as pe:
|
||
outstr = pe.mark_input_line()
|
||
print(outstr)
|
||
self.assertEqual(
|
||
"DOB >!<100-10-2010;more garbage",
|
||
outstr,
|
||
"did not properly create marked input line",
|
||
)
|
||
else:
|
||
self.fail("test construction failed - should have raised an exception")
|
||
|
||
def testDeprecatedNonPep8Names(self):
|
||
word = pp.Word(pp.alphas)
|
||
with self.assertWarns(PyparsingDeprecationWarning):
|
||
word[...].parseString("ABC DEF")
|
||
with self.assertWarns(DeprecationWarning):
|
||
word[...].parseString("ABC DEF")
|
||
|
||
def testLocatedExpr(self):
|
||
# 012345678901234567890123456789012345678901234567890
|
||
samplestr1 = "DOB 10-10-2010;more garbage;ID PARI12345678 ;more garbage"
|
||
|
||
with self.assertWarns(PyparsingDeprecationWarning):
|
||
id_ref = pp.locatedExpr("ID" + pp.Word(pp.alphanums, exact=12)("id"))
|
||
|
||
res = id_ref.search_string(samplestr1)[0][0]
|
||
print(res.dump())
|
||
self.assertEqual(
|
||
"ID PARI12345678",
|
||
samplestr1[res.locn_start : res.locn_end],
|
||
"incorrect location calculation",
|
||
)
|
||
|
||
def testLocatedExprUsingLocated(self):
|
||
# 012345678901234567890123456789012345678901234567890
|
||
samplestr1 = "DOB 10-10-2010;more garbage;ID PARI12345678 ;more garbage"
|
||
|
||
id_ref = pp.Located("ID" + pp.Word(pp.alphanums, exact=12)("id"))
|
||
|
||
res = id_ref.search_string(samplestr1)[0]
|
||
print(res.dump())
|
||
self.assertEqual(
|
||
"ID PARI12345678",
|
||
samplestr1[res.locn_start : res.locn_end],
|
||
"incorrect location calculation",
|
||
)
|
||
self.assertParseResultsEquals(
|
||
res,
|
||
[28, ["ID", "PARI12345678"], 43],
|
||
{"locn_end": 43, "locn_start": 28, "value": {"id": "PARI12345678"}},
|
||
)
|
||
self.assertEqual("PARI12345678", res.value.id)
|
||
|
||
# if Located has a results name, handle appropriately
|
||
id_ref = pp.Located("ID" + pp.Word(pp.alphanums, exact=12)("id"))("loc")
|
||
|
||
res = id_ref.search_string(samplestr1)[0]
|
||
print(res.dump())
|
||
self.assertEqual(
|
||
"ID PARI12345678",
|
||
samplestr1[res.loc.locn_start : res.loc.locn_end],
|
||
"incorrect location calculation",
|
||
)
|
||
self.assertParseResultsEquals(
|
||
res.loc,
|
||
[28, ["ID", "PARI12345678"], 43],
|
||
{"locn_end": 43, "locn_start": 28, "value": {"id": "PARI12345678"}},
|
||
)
|
||
self.assertEqual("PARI12345678", res.loc.value.id)
|
||
|
||
wd = pp.Word(pp.alphas)
|
||
test_string = "ljsdf123lksdjjf123lkkjj1222"
|
||
pp_matches = pp.Located(wd).search_string(test_string)
|
||
re_matches = re.finditer("[a-z]+", test_string)
|
||
for pp_match, re_match in zip(pp_matches, re_matches):
|
||
self.assertParseResultsEquals(
|
||
pp_match, [re_match.start(), [re_match.group(0)], re_match.end()]
|
||
)
|
||
print(pp_match)
|
||
print(re_match)
|
||
print(pp_match.value)
|
||
|
||
def testLocatedExprLeadingWhitespace(self):
|
||
# Located should mark the start of the match, not leading whitespace the
|
||
# wrapped expression skips - including when whitespace skipping is
|
||
# delegated to sub-expressions (MatchFirst, And). Issue #621.
|
||
abc = pp.Keyword("abc")
|
||
|
||
# a single token already reported the correct location
|
||
single = pp.Located(abc).parse_string(" abc")
|
||
self.assertParseResultsEquals(single, [3, ["abc"], 6])
|
||
|
||
# a MatchFirst used to report locn_start=0, a " abc" span
|
||
match_first = pp.Located(abc | abc).parse_string(" abc")
|
||
self.assertParseResultsEquals(match_first, [3, ["abc"], 6])
|
||
self.assertEqual(
|
||
"abc",
|
||
" abc"[match_first.locn_start : match_first.locn_end],
|
||
"Located(MatchFirst) included leading whitespace in its location",
|
||
)
|
||
|
||
# an And likewise delegates whitespace skipping
|
||
sample = " ID PARI12345678"
|
||
seq = pp.Located(pp.Literal("ID") + pp.Word(pp.alphanums)).parse_string(sample)
|
||
self.assertEqual(
|
||
"ID PARI12345678",
|
||
sample[seq.locn_start : seq.locn_end],
|
||
"Located(And) included leading whitespace in its location",
|
||
)
|
||
|
||
# leave_whitespace() must keep the leading whitespace
|
||
leave_ws = pp.Located(pp.Word(" abc").leave_whitespace()).parse_string(" abc")
|
||
self.assertEqual(
|
||
0,
|
||
leave_ws.locn_start,
|
||
"Located should not skip whitespace for a leave_whitespace expression",
|
||
)
|
||
|
||
def testPop(self):
|
||
source = "AAA 123 456 789 234"
|
||
patt = pp.Word(pp.alphas)("name") + pp.Word(pp.nums) * (1,)
|
||
|
||
result = patt.parse_string(source, parse_all=True)
|
||
tests = [
|
||
(0, "AAA", ["123", "456", "789", "234"]),
|
||
(None, "234", ["123", "456", "789"]),
|
||
("name", "AAA", ["123", "456", "789"]),
|
||
(-1, "789", ["123", "456"]),
|
||
]
|
||
for test in tests:
|
||
idx, val, remaining = test
|
||
if idx is not None:
|
||
ret = result.pop(idx)
|
||
else:
|
||
ret = result.pop()
|
||
print("EXP:", val, remaining)
|
||
print("GOT:", ret, result.as_list())
|
||
print(ret, result.as_list())
|
||
self.assertEqual(
|
||
val,
|
||
ret,
|
||
f"wrong value returned, got {ret!r}, expected {val!r}",
|
||
)
|
||
self.assertEqual(
|
||
remaining,
|
||
result.as_list(),
|
||
f"list is in wrong state after pop, got {result.as_list()!r}, expected {remaining!r}",
|
||
)
|
||
print()
|
||
|
||
prevlist = result.as_list()
|
||
ret = result.pop("name", default="noname")
|
||
print(ret)
|
||
print(result.as_list())
|
||
self.assertEqual(
|
||
"noname",
|
||
ret,
|
||
f"default value not successfully returned, got {ret!r}, expected {'noname'!r}",
|
||
)
|
||
self.assertEqual(
|
||
prevlist,
|
||
result.as_list(),
|
||
f"list is in wrong state after pop, got {result.as_list()!r}, expected {remaining!r}",
|
||
)
|
||
|
||
def testPopKwargsErr(self):
|
||
"""raise a TypeError in pop by adding invalid named args"""
|
||
|
||
source = "AAA 123 456 789 234"
|
||
patt = pp.Word(pp.alphas)("name") + pp.Word(pp.nums) * (1,)
|
||
result = patt.parse_string(source, parse_all=True)
|
||
print(result.dump())
|
||
|
||
with self.assertRaises(TypeError):
|
||
result.pop(notDefault="foo")
|
||
|
||
def testAddCondition(self):
|
||
numParser = pp.Word(pp.nums)
|
||
numParser.add_parse_action(lambda s, l, t: int(t[0]))
|
||
numParser.add_condition(lambda s, l, t: t[0] % 2)
|
||
numParser.add_condition(lambda s, l, t: t[0] >= 7)
|
||
|
||
result = numParser.search_string("1 2 3 4 5 6 7 8 9 10")
|
||
print(result.as_list())
|
||
self.assertEqual(
|
||
[[7], [9]], result.as_list(), "failed to properly process conditions"
|
||
)
|
||
|
||
numParser = pp.Word(pp.nums)
|
||
numParser.add_parse_action(lambda s, l, t: int(t[0]))
|
||
rangeParser = numParser("from_") + pp.Suppress("-") + numParser("to")
|
||
|
||
result = rangeParser.search_string("1-4 2-4 4-3 5 6 7 8 9 10")
|
||
print(result.as_list())
|
||
self.assertEqual(
|
||
[[1, 4], [2, 4], [4, 3]],
|
||
result.as_list(),
|
||
"failed to properly process conditions",
|
||
)
|
||
|
||
rangeParser.add_condition(
|
||
lambda t: t.to > t.from_, message="from must be <= to", fatal=False
|
||
)
|
||
result = rangeParser.search_string("1-4 2-4 4-3 5 6 7 8 9 10")
|
||
print(result.as_list())
|
||
self.assertEqual(
|
||
[[1, 4], [2, 4]], result.as_list(), "failed to properly process conditions"
|
||
)
|
||
|
||
rangeParser = numParser("from_") + pp.Suppress("-") + numParser("to")
|
||
rangeParser.add_condition(
|
||
lambda t: t.to > t.from_, message="from must be <= to", fatal=True
|
||
)
|
||
try:
|
||
result = rangeParser.search_string("1-4 2-4 4-3 5 6 7 8 9 10")
|
||
self.fail("failed to interrupt parsing on fatal condition failure")
|
||
except ParseFatalException:
|
||
print("detected fatal condition")
|
||
|
||
def testPatientOr(self):
|
||
# Two expressions and a input string which could - syntactically - be matched against
|
||
# both expressions. The "Literal" expression is considered invalid though, so this PE
|
||
# should always detect the "Word" expression.
|
||
def validate(token):
|
||
if token[0] == "def":
|
||
raise pp.ParseException("signalling invalid token")
|
||
return token
|
||
|
||
a = pp.Word("de").set_name("Word") # .set_debug()
|
||
b = (
|
||
pp.Literal("def").set_name("Literal").set_parse_action(validate)
|
||
) # .set_debug()
|
||
c = pp.Literal("d").set_name("d") # .set_debug()
|
||
|
||
# The "Literal" expressions's ParseAction is not executed directly after syntactically
|
||
# detecting the "Literal" Expression but only after the Or-decision has been made
|
||
# (which is too late)...
|
||
try:
|
||
result = (a ^ b ^ c).parse_string("def", parse_all=False)
|
||
print(result)
|
||
self.assertEqual(
|
||
["de"],
|
||
result.as_list(),
|
||
f"failed to select longest match, chose {result}",
|
||
)
|
||
except ParseException:
|
||
failed = True
|
||
else:
|
||
failed = False
|
||
|
||
if failed:
|
||
self.fail(
|
||
"invalid logic in Or, fails on longest match with exception in parse action"
|
||
)
|
||
|
||
# from issue #93
|
||
word = pp.Word(pp.alphas).set_name("word")
|
||
word_1 = (
|
||
pp.Word(pp.alphas)
|
||
.set_name("word_1")
|
||
.add_condition(lambda t: len(t[0]) == 1)
|
||
)
|
||
|
||
a = word + (word_1 + word ^ word)
|
||
b = word * 3
|
||
c = a ^ b
|
||
c.streamline()
|
||
print(c)
|
||
test_string = "foo bar temp"
|
||
result = c.parse_string(test_string, parse_all=True)
|
||
print(test_string, "->", result.as_list())
|
||
|
||
self.assertEqual(
|
||
test_string.split(), result.as_list(), "failed to match longest choice"
|
||
)
|
||
|
||
def testEachWithOptionalWithResultsName(self):
|
||
result = (pp.Optional("foo")("one") & pp.Optional("bar")("two")).parse_string(
|
||
"bar foo", parse_all=True
|
||
)
|
||
print(result.dump())
|
||
self.assertEqual(sorted(["one", "two"]), sorted(result.keys()))
|
||
|
||
def testUnicodeExpression(self):
|
||
z = "a" | pp.Literal("\u1111")
|
||
z.streamline()
|
||
try:
|
||
z.parse_string("b", parse_all=True)
|
||
except ParseException as pe:
|
||
self.assertEqual(
|
||
r"""Expected {'a' | 'ᄑ'}""",
|
||
pe.msg,
|
||
f"Invalid error message raised, got {pe.msg!r}",
|
||
)
|
||
|
||
def testSetName(self):
|
||
a = pp.one_of("a b c")
|
||
b = pp.one_of("d e f")
|
||
# fmt: off
|
||
arith_expr = pp.infix_notation(
|
||
pp.Word(pp.nums),
|
||
[
|
||
(pp.one_of("* /").set_name("* | /"), 2, pp.OpAssoc.LEFT),
|
||
(pp.one_of("+ -").set_name("+ | -"), 2, pp.OpAssoc.LEFT),
|
||
],
|
||
)
|
||
arith_expr2 = pp.infix_notation(
|
||
pp.Word(pp.nums),
|
||
[
|
||
(("?", ":"), 3, pp.OpAssoc.LEFT),
|
||
]
|
||
)
|
||
# fmt: on
|
||
recursive = pp.Forward()
|
||
recursive <<= a + (b + recursive)[...]
|
||
|
||
tests = [
|
||
a,
|
||
b,
|
||
(a | b),
|
||
arith_expr,
|
||
arith_expr.expr,
|
||
arith_expr2,
|
||
arith_expr2.expr,
|
||
recursive,
|
||
pp.DelimitedList(pp.Word(pp.nums).set_name("int")),
|
||
pp.counted_array(pp.Word(pp.nums).set_name("int")),
|
||
pp.nested_expr(),
|
||
pp.make_html_tags("Z"),
|
||
(pp.any_open_tag, pp.any_close_tag),
|
||
pp.common_html_entity,
|
||
pp.common_html_entity.set_parse_action(
|
||
pp.replace_html_entity
|
||
).transform_string("lsdjkf <lsdjkf>&'"&xyzzy;"),
|
||
]
|
||
|
||
expected = map(
|
||
str.strip,
|
||
"""\
|
||
'a' | 'b' | 'c'
|
||
'd' | 'e' | 'f'
|
||
{'a' | 'b' | 'c' | 'd' | 'e' | 'f'}
|
||
W:(0-9)_expression
|
||
+ | - operations
|
||
W:(0-9)_expression
|
||
?: operations
|
||
Forward: {'a' | 'b' | 'c' [{'d' | 'e' | 'f' : ...}]...}
|
||
int [, int]...
|
||
(len) int...
|
||
nested () expression
|
||
(<Z>, </Z>)
|
||
(<any tag>, </any tag>)
|
||
common HTML entity
|
||
lsdjkf <lsdjkf>&'"&xyzzy;""".splitlines(),
|
||
)
|
||
|
||
for t, e in zip(tests, expected):
|
||
with self.subTest("set_name", t=t, e=e):
|
||
tname = str(t)
|
||
print(tname)
|
||
self.assertEqual(
|
||
e,
|
||
tname,
|
||
f"expression name mismatch, expected {e} got {tname}",
|
||
)
|
||
|
||
def testTrimArityExceptionMasking(self):
|
||
invalid_message = "<lambda>() missing 1 required positional argument: 't'"
|
||
try:
|
||
pp.Word("a").set_parse_action(lambda t: t[0] + 1).parse_string(
|
||
"aaa", parse_all=True
|
||
)
|
||
except Exception as e:
|
||
exc_msg = str(e)
|
||
self.assertNotEqual(
|
||
exc_msg,
|
||
invalid_message,
|
||
"failed to catch TypeError thrown in _trim_arity",
|
||
)
|
||
|
||
def testTrimArityExceptionMaskingTest2(self):
|
||
# construct deep call tree
|
||
def A():
|
||
import traceback
|
||
|
||
traceback.print_stack(limit=2)
|
||
|
||
invalid_message = "<lambda>() missing 1 required positional argument: 't'"
|
||
try:
|
||
pp.Word("a").set_parse_action(lambda t: t[0] + 1).parse_string(
|
||
"aaa", parse_all=True
|
||
)
|
||
except Exception as e:
|
||
exc_msg = str(e)
|
||
self.assertNotEqual(
|
||
exc_msg,
|
||
invalid_message,
|
||
"failed to catch TypeError thrown in _trim_arity",
|
||
)
|
||
|
||
def B():
|
||
A()
|
||
|
||
def C():
|
||
B()
|
||
|
||
def D():
|
||
C()
|
||
|
||
def E():
|
||
D()
|
||
|
||
def F():
|
||
E()
|
||
|
||
def G():
|
||
F()
|
||
|
||
def H():
|
||
G()
|
||
|
||
def J():
|
||
H()
|
||
|
||
def K():
|
||
J()
|
||
|
||
K()
|
||
|
||
def testClearParseActions(self):
|
||
realnum = ppc.real()
|
||
self.assertEqual(
|
||
3.14159,
|
||
realnum.parse_string("3.14159", parse_all=True)[0],
|
||
"failed basic real number parsing",
|
||
)
|
||
|
||
# clear parse action that converts to float
|
||
realnum.set_parse_action(None)
|
||
self.assertEqual(
|
||
"3.14159",
|
||
realnum.parse_string("3.14159", parse_all=True)[0],
|
||
"failed clearing parse action",
|
||
)
|
||
|
||
# add a new parse action that tests if a '.' is prsent
|
||
realnum.add_parse_action(lambda t: "." in t[0])
|
||
self.assertEqual(
|
||
True,
|
||
realnum.parse_string("3.14159", parse_all=True)[0],
|
||
"failed setting new parse action after clearing parse action",
|
||
)
|
||
|
||
def testOneOrMoreStop(self):
|
||
test = "BEGIN aaa bbb ccc END"
|
||
BEGIN, END = map(pp.Keyword, "BEGIN,END".split(","))
|
||
body_word = pp.Word(pp.alphas).set_name("word")
|
||
for ender in (END, "END", pp.CaselessKeyword("END")):
|
||
expr = BEGIN + pp.OneOrMore(body_word, stop_on=ender) + END
|
||
self.assertEqual(
|
||
expr, test, f"Did not successfully stop on ending expression {ender!r}"
|
||
)
|
||
|
||
expr = BEGIN + body_word[1, ...].stopOn(ender) + END
|
||
self.assertParseAndCheckList(
|
||
expr,
|
||
test,
|
||
test.split(),
|
||
f"Did not successfully stop on ending expression {ender!r}",
|
||
)
|
||
|
||
expr = BEGIN + body_word[1, ...:ender] + END
|
||
self.assertParseAndCheckList(
|
||
expr,
|
||
test,
|
||
test.split(),
|
||
f"Did not successfully stop on ending expression {ender!r}",
|
||
)
|
||
|
||
expr = BEGIN + body_word[(1, ...):ender] + END
|
||
self.assertParseAndCheckList(
|
||
expr,
|
||
test,
|
||
test.split(),
|
||
f"Did not successfully stop on ending expression {ender!r}",
|
||
)
|
||
|
||
number = pp.Word(pp.nums + ",.()").set_name("number with optional commas")
|
||
parser = pp.OneOrMore(pp.Word(pp.alphanums + "-/."), stop_on=number)(
|
||
"id"
|
||
).set_parse_action(" ".join) + number("data")
|
||
self.assertParseAndCheckList(
|
||
parser,
|
||
" XXX Y/123 1,234.567890",
|
||
["XXX Y/123", "1,234.567890"],
|
||
f"Did not successfully stop on ending expression {number!r}",
|
||
verbose=True,
|
||
)
|
||
|
||
def testZeroOrMoreStop(self):
|
||
test = "BEGIN END"
|
||
BEGIN, END = map(pp.Keyword, "BEGIN,END".split(","))
|
||
body_word = pp.Word(pp.alphas).set_name("word")
|
||
for ender in (END, "END", pp.CaselessKeyword("END")):
|
||
expr = BEGIN + pp.ZeroOrMore(body_word, stop_on=ender) + END
|
||
self.assertParseAndCheckList(
|
||
expr,
|
||
test,
|
||
test.split(),
|
||
f"Did not successfully stop on ending expression {ender!r}",
|
||
)
|
||
|
||
expr = BEGIN + body_word[...].stopOn(ender) + END
|
||
self.assertParseAndCheckList(
|
||
expr,
|
||
test,
|
||
test.split(),
|
||
f"Did not successfully stop on ending expression {ender!r}",
|
||
)
|
||
|
||
expr = BEGIN + body_word[...:ender] + END
|
||
self.assertParseAndCheckList(
|
||
expr,
|
||
test,
|
||
test.split(),
|
||
f"Did not successfully stop on ending expression {ender!r}",
|
||
)
|
||
|
||
expr = BEGIN + body_word[:ender] + END
|
||
self.assertParseAndCheckList(
|
||
expr,
|
||
test,
|
||
test.split(),
|
||
f"Did not successfully stop on ending expression {ender!r}",
|
||
)
|
||
|
||
def testOneOrMoreMax(self):
|
||
# Test OneOrMore with max
|
||
expr = pp.OneOrMore(pp.Word(pp.nums), max=3)
|
||
|
||
# Should match 1, 2, or 3
|
||
self.assertEqual(len(expr.parse_string("1")), 1)
|
||
self.assertEqual(len(expr.parse_string("1 2")), 2)
|
||
self.assertEqual(len(expr.parse_string("1 2 3")), 3)
|
||
|
||
# Should match ONLY 3 and leave the rest
|
||
res = expr.parse_string("1 2 3 4 5")
|
||
self.assertEqual(len(res), 3)
|
||
self.assertEqual(res.as_list(), ["1", "2", "3"])
|
||
|
||
# Test max=1
|
||
expr = pp.OneOrMore(pp.Word(pp.nums), max=1)
|
||
res = expr.parse_string("1 2 3")
|
||
self.assertEqual(len(res), 1)
|
||
|
||
# Test max=0 (should raise ValueError)
|
||
with self.assertRaises(ValueError):
|
||
pp.OneOrMore(pp.Word(pp.nums), max=0)
|
||
|
||
def testZeroOrMoreMax(self):
|
||
# Test ZeroOrMore with max
|
||
expr = pp.ZeroOrMore(pp.Word(pp.nums), max=3)
|
||
|
||
# Should match 0, 1, 2, or 3
|
||
self.assertEqual(len(expr.parse_string("")), 0)
|
||
self.assertEqual(len(expr.parse_string("1")), 1)
|
||
self.assertEqual(len(expr.parse_string("1 2")), 2)
|
||
self.assertEqual(len(expr.parse_string("1 2 3")), 3)
|
||
|
||
# Should match ONLY 3 and leave the rest
|
||
res = expr.parse_string("1 2 3 4 5")
|
||
self.assertEqual(len(res), 3)
|
||
self.assertEqual(res.as_list(), ["1", "2", "3"])
|
||
|
||
# Test max=1
|
||
expr = pp.ZeroOrMore(pp.Word(pp.nums), max=1)
|
||
res = expr.parse_string("1 2 3")
|
||
self.assertEqual(len(res), 1)
|
||
|
||
# Test max=0 (should raise ValueError)
|
||
with self.assertRaises(ValueError):
|
||
pp.ZeroOrMore(pp.Word(pp.nums), max=0)
|
||
|
||
def testOneOrMoreMaxNegative(self):
|
||
# If max is negative, OneOrMore should raise ValueError
|
||
with self.assertRaises(ValueError):
|
||
pp.OneOrMore(pp.Word(pp.nums), max=-1)
|
||
|
||
def testZeroOrMoreMaxNegative(self):
|
||
# If max is negative, ZeroOrMore should raise ValueError
|
||
with self.assertRaises(ValueError):
|
||
pp.ZeroOrMore(pp.Word(pp.nums), max=-1)
|
||
|
||
def testNestedAsDict(self):
|
||
equals = pp.Literal("=").suppress()
|
||
lbracket = pp.Literal("[").suppress()
|
||
rbracket = pp.Literal("]").suppress()
|
||
lbrace = pp.Literal("{").suppress()
|
||
rbrace = pp.Literal("}").suppress()
|
||
|
||
value_dict = pp.Forward()
|
||
value_list = pp.Forward()
|
||
value_string = pp.Word(pp.alphanums + "@. ")
|
||
|
||
value = value_list ^ value_dict ^ value_string
|
||
values = pp.Group(pp.DelimitedList(value, ","))
|
||
# ~ values = DelimitedList(value, ",").set_parse_action(lambda toks: [toks.as_list()])
|
||
|
||
value_list <<= lbracket + values + rbracket
|
||
|
||
identifier = pp.Word(pp.alphanums + "_.")
|
||
|
||
assignment = pp.Group(identifier + equals + pp.Optional(value))
|
||
assignments = pp.Dict(pp.DelimitedList(assignment, ";"))
|
||
value_dict <<= lbrace + assignments + rbrace
|
||
|
||
response = assignments
|
||
|
||
rsp = (
|
||
"username=goat; errors={username=[already taken, too short]}; empty_field="
|
||
)
|
||
result_dict = response.parse_string(rsp, parse_all=True).as_dict()
|
||
print(result_dict)
|
||
self.assertEqual(
|
||
"goat",
|
||
result_dict["username"],
|
||
"failed to process string in ParseResults correctly",
|
||
)
|
||
self.assertEqual(
|
||
["already taken", "too short"],
|
||
result_dict["errors"]["username"],
|
||
"failed to process nested ParseResults correctly",
|
||
)
|
||
|
||
def testTraceParseActionDecorator(self):
|
||
@pp.trace_parse_action
|
||
def convert_to_int(t):
|
||
return int(t[0])
|
||
|
||
class Z:
|
||
def __call__(self, other):
|
||
return other[0] * 1000
|
||
|
||
integer = pp.Word(pp.nums).add_parse_action(convert_to_int)
|
||
integer.add_parse_action(pp.trace_parse_action(lambda t: t[0] * 10))
|
||
integer.add_parse_action(pp.trace_parse_action(Z()))
|
||
integer.parse_string("132", parse_all=True)
|
||
|
||
def testTraceParseActionDecorator_with_exception(self):
|
||
@pp.trace_parse_action
|
||
def convert_to_int_raising_type_error(t):
|
||
return int(t[0]) + ".000"
|
||
|
||
@pp.trace_parse_action
|
||
def convert_to_int_raising_index_error(t):
|
||
return int(t[1])
|
||
|
||
@pp.trace_parse_action
|
||
def convert_to_int_raising_value_error(t):
|
||
a, b = t[0]
|
||
return int(t[1])
|
||
|
||
@pp.trace_parse_action
|
||
def convert_to_int_raising_parse_exception(t):
|
||
pp.Word(pp.alphas).parse_string("123")
|
||
|
||
for pa, expected_message in (
|
||
(convert_to_int_raising_type_error, "TypeError:"),
|
||
(convert_to_int_raising_index_error, "IndexError:"),
|
||
(convert_to_int_raising_value_error, "ValueError:"),
|
||
(convert_to_int_raising_parse_exception, "ParseException:"),
|
||
):
|
||
print(f"Using parse action {pa.__name__!r}")
|
||
integer = pp.Word(pp.nums).set_parse_action(pa)
|
||
stderr_capture = StringIO()
|
||
try:
|
||
with contextlib.redirect_stderr(stderr_capture):
|
||
integer.parse_string("132", parse_all=True)
|
||
except Exception as exc:
|
||
print(f"Exception raised: {type(exc).__name__}: {exc}")
|
||
else:
|
||
print("No exception raised")
|
||
stderr_text = stderr_capture.getvalue()
|
||
print(stderr_text)
|
||
self.assertTrue(
|
||
expected_message in stderr_text,
|
||
f"Expected exception type {expected_message!r} not found in trace_parse_action output",
|
||
)
|
||
|
||
def testRunTests(self):
|
||
integer = pp.Word(pp.nums).set_parse_action(lambda t: int(t[0]))
|
||
intrange = integer("start") + "-" + integer("end")
|
||
intrange.add_condition(
|
||
lambda t: t.end > t.start,
|
||
message="invalid range, start must be <= end",
|
||
fatal=True,
|
||
)
|
||
intrange.add_parse_action(lambda t: list(range(t.start, t.end + 1)))
|
||
|
||
indices = pp.DelimitedList(intrange | integer)
|
||
indices.add_parse_action(lambda t: sorted(set(t)))
|
||
|
||
tests = """\
|
||
# normal data
|
||
1-3,2-4,6,8-10,16
|
||
|
||
# lone integer
|
||
11"""
|
||
results = indices.run_tests(tests, print_results=False)[1]
|
||
|
||
expectedResults = [[1, 2, 3, 4, 6, 8, 9, 10, 16], [11]]
|
||
for res, expected in zip(results, expectedResults):
|
||
print(res[1].as_list())
|
||
print(expected)
|
||
self.assertEqual(
|
||
expected, res[1].as_list(), "failed test: " + str(expected)
|
||
)
|
||
|
||
tests = """\
|
||
# invalid range
|
||
1-2, 3-1, 4-6, 7, 12
|
||
"""
|
||
success, _ = indices.run_tests(tests, print_results=False, failure_tests=True)
|
||
self.assertTrue(success, "failed to raise exception on improper range test")
|
||
|
||
def testRunTestsPostParse(self):
|
||
integer = ppc.integer
|
||
fraction = integer("numerator") + "/" + integer("denominator")
|
||
|
||
accum = []
|
||
|
||
def eval_fraction(test, result):
|
||
accum.append((test, result.as_list()))
|
||
return f"eval: {result.numerator / result.denominator}"
|
||
|
||
success, _ = fraction.run_tests(
|
||
"""\
|
||
1/2
|
||
1/0
|
||
""",
|
||
post_parse=eval_fraction,
|
||
)
|
||
self.assertTrue(success, "failed to parse fractions in RunTestsPostParse")
|
||
|
||
expected_accum = [("1/2", [1, "/", 2]), ("1/0", [1, "/", 0])]
|
||
self.assertEqual(
|
||
expected_accum, accum, "failed to call post_parse method during run_tests"
|
||
)
|
||
|
||
def testConvertToDateErr(self):
|
||
"""raise a ParseException in convert_to_date with incompatible date str"""
|
||
|
||
expr = pp.Word(pp.alphanums + "-")
|
||
expr.add_parse_action(ppc.convert_to_date())
|
||
|
||
with self.assertRaisesParseException():
|
||
expr.parse_string("1997-07-error", parse_all=True)
|
||
|
||
def testConvertToDatetimeErr(self):
|
||
"""raise a ParseException in convert_to_datetime with incompatible datetime str"""
|
||
|
||
expr = pp.Word(pp.alphanums + "-")
|
||
expr.add_parse_action(ppc.convert_to_datetime())
|
||
|
||
with self.assertRaisesParseException():
|
||
expr.parse_string("1997-07-error", parse_all=True)
|
||
|
||
def testCommonExpressions(self):
|
||
import ast
|
||
|
||
with self.subTest("MAC address success run_tests"):
|
||
success, _ = ppc.mac_address.run_tests(
|
||
"""
|
||
AA:BB:CC:DD:EE:FF
|
||
AA.BB.CC.DD.EE.FF
|
||
AA-BB-CC-DD-EE-FF
|
||
"""
|
||
)
|
||
self.assertTrue(success, "error in parsing valid MAC address")
|
||
|
||
with self.subTest("MAC address expected failure run_tests"):
|
||
success, _ = ppc.mac_address.run_tests(
|
||
"""
|
||
# mixed delimiters
|
||
AA.BB:CC:DD:EE:FF
|
||
""",
|
||
failure_tests=True,
|
||
)
|
||
self.assertTrue(success, "error in detecting invalid mac address")
|
||
|
||
with self.subTest("IPv4 address success run_tests"):
|
||
success, _ = ppc.ipv4_address.run_tests(
|
||
"""
|
||
0.0.0.0
|
||
1.1.1.1
|
||
127.0.0.1
|
||
1.10.100.199
|
||
255.255.255.255
|
||
"""
|
||
)
|
||
self.assertTrue(success, "error in parsing valid IPv4 address")
|
||
|
||
with self.subTest("IPv4 address expected failure run_tests"):
|
||
success, _ = ppc.ipv4_address.run_tests(
|
||
"""
|
||
# out of range value
|
||
256.255.255.255
|
||
""",
|
||
failure_tests=True,
|
||
)
|
||
self.assertTrue(success, "error in detecting invalid IPv4 address")
|
||
|
||
with self.subTest("IPv6 address success run_tests"):
|
||
success, _ = ppc.ipv6_address.run_tests(
|
||
"""
|
||
2001:0db8:85a3:0000:0000:8a2e:0370:7334
|
||
2134::1234:4567:2468:1236:2444:2106
|
||
0:0:0:0:0:0:A00:1
|
||
1080::8:800:200C:417A
|
||
::A00:1
|
||
|
||
# loopback address
|
||
::1
|
||
|
||
# the null address
|
||
::
|
||
|
||
# ipv4 compatibility form
|
||
::ffff:192.168.0.1
|
||
"""
|
||
)
|
||
self.assertTrue(success, "error in parsing valid IPv6 address")
|
||
|
||
with self.subTest("IPv6 address expected failure run_tests"):
|
||
success, _ = ppc.ipv6_address.run_tests(
|
||
"""
|
||
# too few values
|
||
1080:0:0:0:8:800:200C
|
||
|
||
# too many ::'s, only 1 allowed
|
||
2134::1234:4567::2444:2106
|
||
""",
|
||
failure_tests=True,
|
||
)
|
||
self.assertTrue(success, "error in detecting invalid IPv6 address")
|
||
|
||
with self.subTest("ppc.number success run_tests"):
|
||
success, _ = ppc.number.run_tests(
|
||
"""
|
||
100
|
||
-100
|
||
+100
|
||
3.14159
|
||
6.02e23
|
||
1e-12
|
||
"""
|
||
)
|
||
self.assertTrue(success, "error in parsing valid numerics")
|
||
|
||
with self.subTest("ppc.sci_real success run_tests"):
|
||
success, _ = ppc.sci_real.run_tests(
|
||
"""
|
||
1e12
|
||
-1e12
|
||
3.14159
|
||
6.02e23
|
||
"""
|
||
)
|
||
self.assertTrue(success, "error in parsing valid scientific notation reals")
|
||
|
||
# any int or real number, returned as float
|
||
with self.subTest("ppc.fnumber success run_tests"):
|
||
success, _ = ppc.fnumber.run_tests(
|
||
"""
|
||
100
|
||
-100
|
||
+100
|
||
3.14159
|
||
6.02e23
|
||
1e-12
|
||
"""
|
||
)
|
||
self.assertTrue(success, "error in parsing valid numerics")
|
||
|
||
with self.subTest("ppc.ieee_float success run_tests"):
|
||
success, _ = ppc.ieee_float.run_tests(
|
||
"""
|
||
100
|
||
3.14159
|
||
6.02e23
|
||
1E-12
|
||
0
|
||
-0
|
||
NaN
|
||
-nan
|
||
inf
|
||
-Infinity
|
||
"""
|
||
)
|
||
self.assertTrue(success, "error in parsing valid floating-point literals")
|
||
|
||
with self.subTest("ppc.iso8601_date success run_tests"):
|
||
success, results = ppc.iso8601_date.run_tests(
|
||
"""
|
||
1997
|
||
1997-07
|
||
1997-07-16
|
||
"""
|
||
)
|
||
self.assertTrue(success, "error in parsing valid iso8601_date")
|
||
expected = [
|
||
("1997", None, None),
|
||
("1997", "07", None),
|
||
("1997", "07", "16"),
|
||
]
|
||
for r, exp in zip(results, expected):
|
||
self.assertEqual(
|
||
exp,
|
||
(r[1].year, r[1].month, r[1].day),
|
||
"failed to parse date into fields",
|
||
)
|
||
|
||
with self.subTest("ppc.iso8601_date conversion success run_tests"):
|
||
success, results = (
|
||
ppc.iso8601_date()
|
||
.add_parse_action(ppc.convert_to_date())
|
||
.run_tests(
|
||
"""
|
||
1997-07-16
|
||
"""
|
||
)
|
||
)
|
||
self.assertTrue(
|
||
success, "error in parsing valid iso8601_date with parse action"
|
||
)
|
||
self.assertEqual(
|
||
datetime.date(1997, 7, 16),
|
||
results[0][1][0],
|
||
"error in parsing valid iso8601_date with parse action - incorrect value",
|
||
)
|
||
|
||
with self.subTest("ppc.iso8601_datetime success run_tests"):
|
||
success, results = ppc.iso8601_datetime.run_tests(
|
||
"""
|
||
1997-07-16T19:20+01:00
|
||
1997-07-16T19:20:30+01:00
|
||
1997-07-16T19:20:30.45Z
|
||
1997-07-16 19:20:30.45
|
||
"""
|
||
)
|
||
self.assertTrue(success, "error in parsing valid iso8601_datetime")
|
||
|
||
with self.subTest("ppc.iso8601_datetime conversion success run_tests"):
|
||
success, results = (
|
||
ppc.iso8601_datetime()
|
||
.add_parse_action(ppc.convert_to_datetime())
|
||
.run_tests(
|
||
"""
|
||
1997-07-16T19:20:30.45
|
||
"""
|
||
)
|
||
)
|
||
|
||
self.assertTrue(success, "error in parsing valid iso8601_datetime")
|
||
self.assertEqual(
|
||
datetime.datetime(1997, 7, 16, 19, 20, 30, 450000),
|
||
results[0][1][0],
|
||
"error in parsing valid iso8601_datetime - incorrect value",
|
||
)
|
||
|
||
with self.subTest("ppc.as_datetime fractional seconds run_tests"):
|
||
success, results = (
|
||
ppc.iso8601_datetime()
|
||
.add_parse_action(ppc.as_datetime)
|
||
.run_tests(
|
||
"""
|
||
1997-07-16T19:20:30.45
|
||
"""
|
||
)
|
||
)
|
||
|
||
self.assertTrue(success, "error in parsing valid iso8601_datetime")
|
||
# as_datetime must scale fractional seconds to microseconds, matching
|
||
# convert_to_datetime above (0.45 s -> 450000 us, not 449 us).
|
||
self.assertEqual(
|
||
datetime.datetime(1997, 7, 16, 19, 20, 30, 450000),
|
||
results[0][1][0],
|
||
"error in as_datetime fractional seconds - incorrect microseconds",
|
||
)
|
||
|
||
with self.subTest(
|
||
"ppc.as_datetime fractional seconds rounding up carry into next second"
|
||
):
|
||
# A fractional-seconds value that rounds up to a full second (e.g. the
|
||
# trailing digits of a nanosecond-precision timestamp) must carry into
|
||
# the next second, not overflow datetime's 0..999999 microsecond arg.
|
||
parse_dt = ppc.iso8601_datetime().add_parse_action(ppc.as_datetime)
|
||
self.assertEqual(
|
||
datetime.datetime(2021, 1, 1, 0, 0, 1),
|
||
parse_dt.parse_string("2021-01-01T00:00:00.999999999")[0],
|
||
"as_datetime must carry a rounded-up fractional second, not raise",
|
||
)
|
||
self.assertEqual(
|
||
datetime.datetime(2021, 6, 15, 12, 31, 0),
|
||
parse_dt.parse_string("2021-06-15T12:30:59.9999995")[0],
|
||
"as_datetime must carry a rounded-up fractional second across minutes",
|
||
)
|
||
|
||
with self.subTest(
|
||
"ppc.as_datetime ParseException receives input string, not tokens"
|
||
):
|
||
# as_datetime should raise ParseException with the input string
|
||
# as pstr, not the ParseResults tokens, so that the exception
|
||
# message can be formatted without a secondary TypeError.
|
||
result = ppc.iso8601_datetime.parse_string("1997-13-01T00:00:00")
|
||
msg = ""
|
||
try:
|
||
ppc.as_datetime("", 0, result)
|
||
except ParseException as pe:
|
||
msg = str(pe)
|
||
except Exception:
|
||
pass
|
||
self.assertNotIn(
|
||
"TypeError",
|
||
msg,
|
||
"ParseException string must not contain TypeError "
|
||
"(pstr should be the input string, not tokens).",
|
||
)
|
||
|
||
with self.subTest("ppc.uuid success run_tests"):
|
||
success, _ = ppc.uuid.run_tests(
|
||
"""
|
||
123e4567-e89b-12d3-a456-426655440000
|
||
"""
|
||
)
|
||
self.assertTrue(success, "failed to parse valid uuid")
|
||
|
||
with self.subTest("ppc.fraction success run_tests"):
|
||
success, _ = ppc.fraction.run_tests(
|
||
"""
|
||
1/2
|
||
-15/16
|
||
-3/-4
|
||
"""
|
||
)
|
||
self.assertTrue(success, "failed to parse valid fraction")
|
||
|
||
with self.subTest("ppc.mixed_integer success run_tests"):
|
||
success, _ = ppc.mixed_integer.run_tests(
|
||
"""
|
||
1/2
|
||
-15/16
|
||
-3/-4
|
||
1 1/2
|
||
2 -15/16
|
||
0 -3/-4
|
||
12
|
||
"""
|
||
)
|
||
self.assertTrue(success, "failed to parse valid mixed integer")
|
||
|
||
with self.subTest("ppc.number success run_tests"):
|
||
success, results = ppc.number.run_tests(
|
||
"""
|
||
100
|
||
-3
|
||
1.732
|
||
-3.14159
|
||
6.02e23"""
|
||
)
|
||
self.assertTrue(success, "failed to parse numerics")
|
||
|
||
for test, result in results:
|
||
expected = ast.literal_eval(test)
|
||
self.assertEqual(
|
||
expected,
|
||
result[0],
|
||
f"numeric parse failed (wrong value) ({result[0]} should be {expected})",
|
||
)
|
||
self.assertEqual(
|
||
type(expected),
|
||
type(result[0]),
|
||
f"numeric parse failed (wrong type) ({type(result[0])} should be {type(expected)})",
|
||
)
|
||
|
||
def testDateTimeValidation(self):
|
||
if sys.version_info[:2] < (3, 10):
|
||
return
|
||
|
||
ppc = pp.pyparsing_common
|
||
date_expr = ppc.iso8601_date_validated
|
||
datetime_expr = ppc.iso8601_datetime_validated
|
||
|
||
valid_dates = [
|
||
"2023-01-01",
|
||
"2024-02-29", # Leap year
|
||
"2000-02-29", # Century leap year
|
||
"1900-01-01",
|
||
"9999-12-31",
|
||
"2023-12", # Optional day
|
||
"2023", # Optional month and day
|
||
]
|
||
for d in valid_dates:
|
||
with self.subTest(date=d):
|
||
date_expr.parse_string(d, parse_all=True)
|
||
|
||
invalid_dates = [
|
||
"2023-02-29", # Not a leap year
|
||
"2023-04-31", # April has 30 days
|
||
"2023-13-01", # Invalid month
|
||
"2023-00-01", # Invalid month
|
||
"2023-01-32", # Invalid day
|
||
"2023-01-00", # Invalid day
|
||
"1900-02-29", # 1900 was not a leap year
|
||
]
|
||
for d in invalid_dates:
|
||
with self.subTest(date=d):
|
||
with self.assertRaises(pp.ParseException):
|
||
date_expr.parse_string(d, parse_all=True)
|
||
|
||
valid_datetimes = [
|
||
"2023-01-01T12:00:00",
|
||
"2023-01-01 12:00:00",
|
||
"2023-01-01T12:00:00Z",
|
||
"2023-01-01T12:00:00+05:00",
|
||
"2023-01-01T12:00:00-05:00",
|
||
"2023-01-01T12:00:00.123",
|
||
"2023-01-01T12:00:00.123Z",
|
||
"2024-02-29T23:59:59",
|
||
]
|
||
for dt in valid_datetimes:
|
||
with self.subTest(datetime=dt):
|
||
datetime_expr.parse_string(dt, parse_all=True)
|
||
|
||
invalid_datetimes = [
|
||
"2023-02-29T12:00:00", # Invalid date
|
||
"2023-01-01T24:00:00", # Invalid hour
|
||
"2023-01-01T12:60:00", # Invalid minute
|
||
"2023-01-01T12:00:60", # Invalid second
|
||
# "2023-01-01T12:00:00+24:00", # Invalid timezone (Regex might catch it or not, valid_date_time doesn't check it)
|
||
]
|
||
for dt in invalid_datetimes:
|
||
with self.subTest(datetime=dt):
|
||
with self.assertRaises(pp.ParseException):
|
||
datetime_expr.parse_string(dt, parse_all=True)
|
||
|
||
def testCommonUrl(self):
|
||
url_good_tests = """\
|
||
http://foo.com/blah_blah
|
||
http://foo.com/blah_blah/
|
||
http://foo.com/blah_blah_(wikipedia)
|
||
http://foo.com/blah_blah_(wikipedia)_(again)
|
||
http://www.example.com/wpstyle/?p=364
|
||
https://www.example.com/foo/?bar=baz&inga=42&quux
|
||
http://✪df.ws/123
|
||
http://userid:password@example.com:8080
|
||
http://userid:password@example.com:8080/
|
||
http://userid@example.com
|
||
http://userid@example.com/
|
||
http://userid@example.com:8080
|
||
http://userid@example.com:8080/
|
||
http://userid:password@example.com
|
||
http://userid:password@example.com/
|
||
http://142.42.1.1/
|
||
http://142.42.1.1:8080/
|
||
http://➡.ws/䨹
|
||
http://⌘.ws
|
||
http://⌘.ws/
|
||
http://foo.com/blah_(wikipedia)#cite-1
|
||
http://foo.com/blah_(wikipedia)_blah#cite-1
|
||
http://foo.com/unicode_(✪)_in_parens
|
||
http://foo.com/(something)?after=parens
|
||
http://☺.damowmow.com/
|
||
http://code.google.com/events/#&product=browser
|
||
http://j.mp
|
||
ftp://foo.bar/baz
|
||
http://foo.bar/?q=Test%20URL-encoded%20stuff
|
||
http://مثال.إختبار
|
||
"""
|
||
success, report = ppc.url.run_tests(url_good_tests)
|
||
self.assertTrue(success)
|
||
|
||
url_bad_tests = """\
|
||
http://
|
||
http://.
|
||
http://..
|
||
http://../
|
||
http://?
|
||
http://??
|
||
http://??/
|
||
http://#
|
||
http://##
|
||
http://##/
|
||
# skip: http://foo.bar?q=Spaces should be encoded
|
||
//
|
||
//a
|
||
///a
|
||
///
|
||
http:///a
|
||
foo.com
|
||
rdar://1234
|
||
h://test
|
||
http:// shouldfail.com
|
||
|
||
:// should fail
|
||
http://foo.bar/foo(bar)baz quux
|
||
ftps://foo.bar/
|
||
http://-error-.invalid/
|
||
# skip: http://a.b--c.de/
|
||
http://-a.b.co
|
||
http://a.b-.co
|
||
http://0.0.0.0
|
||
http://10.1.1.0
|
||
http://10.1.1.255
|
||
http://224.1.1.1
|
||
http://1.1.1.1.1
|
||
http://123.123.123
|
||
http://3628126748
|
||
http://.www.foo.bar/
|
||
# skip: http://www.foo.bar./
|
||
http://.www.foo.bar./
|
||
http://10.1.1.1
|
||
"""
|
||
success, report = ppc.url.run_tests(url_bad_tests, failure_tests=True)
|
||
self.assertTrue(success)
|
||
|
||
def testCommonUrlParts(self):
|
||
from urllib.parse import urlparse
|
||
|
||
sample_url = "https://bob:secret@www.example.com:8080/path/to/resource?filter=int#book-mark"
|
||
|
||
parts = urlparse(sample_url)
|
||
expected = {
|
||
"scheme": parts.scheme,
|
||
"auth": f"{parts.username}:{parts.password}",
|
||
"host": parts.hostname,
|
||
"port": str(parts.port),
|
||
"path": parts.path,
|
||
"query": parts.query,
|
||
"fragment": parts.fragment,
|
||
"url": sample_url,
|
||
}
|
||
|
||
self.assertParseAndCheckDict(ppc.url, sample_url, expected, verbose=True)
|
||
|
||
def testCommonUrlExprs(self):
|
||
def extract_parts(s, split=" "):
|
||
return [[_.strip(split)] for _ in s.strip(split).split(split)]
|
||
|
||
test_string = "http://example.com https://blah.org "
|
||
self.assertParseAndCheckList(
|
||
pp.Group(ppc.url)[...], test_string, extract_parts(test_string)
|
||
)
|
||
|
||
test_string = test_string.replace(" ", " , ")
|
||
self.assertParseAndCheckList(
|
||
pp.DelimitedList(pp.Group(ppc.url), allow_trailing_delim=True),
|
||
test_string,
|
||
extract_parts(test_string, " , "),
|
||
)
|
||
|
||
def testNumericExpressions(self):
|
||
# disable parse actions that do type conversion so we don't accidentally trigger
|
||
# conversion exceptions when what we want to check is the parsing expression
|
||
real = ppc.real().set_parse_action(None)
|
||
sci_real = ppc.sci_real().set_parse_action(None)
|
||
signed_integer = ppc.signed_integer().set_parse_action(None)
|
||
|
||
from itertools import product
|
||
|
||
def make_tests():
|
||
leading_sign = ["+", "-", ""]
|
||
leading_digit = ["0", ""]
|
||
dot = [".", ""]
|
||
decimal_digit = ["1", ""]
|
||
e = ["e", "E", ""]
|
||
e_sign = ["+", "-", ""]
|
||
e_int = ["22", ""]
|
||
stray = ["9", ".", ""]
|
||
|
||
seen = set()
|
||
seen.add("")
|
||
for parts in product(
|
||
leading_sign,
|
||
stray,
|
||
leading_digit,
|
||
dot,
|
||
decimal_digit,
|
||
stray,
|
||
e,
|
||
e_sign,
|
||
e_int,
|
||
stray,
|
||
):
|
||
parts_str = "".join(parts).strip()
|
||
if parts_str in seen:
|
||
continue
|
||
seen.add(parts_str)
|
||
yield parts_str
|
||
|
||
print(len(seen) - 1, "tests produced")
|
||
|
||
# collect tests into valid/invalid sets, depending on whether they evaluate to valid Python floats or ints
|
||
valid_ints = set()
|
||
valid_reals = set()
|
||
valid_sci_reals = set()
|
||
invalid_ints = set()
|
||
invalid_reals = set()
|
||
invalid_sci_reals = set()
|
||
|
||
# check which strings parse as valid floats or ints, and store in related valid or invalid test sets
|
||
for test_str in make_tests():
|
||
if "." in test_str or "e" in test_str.lower():
|
||
try:
|
||
float(test_str)
|
||
except ValueError:
|
||
invalid_sci_reals.add(test_str)
|
||
if "e" not in test_str.lower():
|
||
invalid_reals.add(test_str)
|
||
else:
|
||
valid_sci_reals.add(test_str)
|
||
if "e" not in test_str.lower():
|
||
valid_reals.add(test_str)
|
||
|
||
try:
|
||
int(test_str)
|
||
except ValueError:
|
||
invalid_ints.add(test_str)
|
||
else:
|
||
valid_ints.add(test_str)
|
||
|
||
# now try all the test sets against their respective expressions
|
||
all_pass = True
|
||
suppress_results = {"print_results": False}
|
||
for expr, tests, is_fail, fn in zip(
|
||
[real, sci_real, signed_integer] * 2,
|
||
[
|
||
valid_reals,
|
||
valid_sci_reals,
|
||
valid_ints,
|
||
invalid_reals,
|
||
invalid_sci_reals,
|
||
invalid_ints,
|
||
],
|
||
[False, False, False, True, True, True],
|
||
[float, float, int] * 2,
|
||
):
|
||
#
|
||
# success, test_results = expr.run_tests(sorted(tests, key=len), failure_tests=is_fail, **suppress_results)
|
||
# filter_result_fn = (lambda r: isinstance(r, Exception),
|
||
# lambda r: not isinstance(r, Exception))[is_fail]
|
||
# print(expr, ('FAIL', 'PASS')[success], "{}valid tests ({})".format(len(tests),
|
||
# 'in' if is_fail else ''))
|
||
# if not success:
|
||
# all_pass = False
|
||
# for test_string, result in test_results:
|
||
# if filter_result_fn(result):
|
||
# try:
|
||
# test_value = fn(test_string)
|
||
# except ValueError as ve:
|
||
# test_value = str(ve)
|
||
# print("{!r}: {} {} {}".format(test_string, result,
|
||
# expr.matches(test_string, parse_all=True), test_value))
|
||
|
||
success = True
|
||
for t in tests:
|
||
if expr.matches(t, parse_all=True):
|
||
if is_fail:
|
||
print(t, "should fail but did not")
|
||
success = False
|
||
else:
|
||
if not is_fail:
|
||
print(t, "should not fail but did")
|
||
success = False
|
||
print(
|
||
expr,
|
||
("FAIL", "PASS")[success],
|
||
f"{'in' if is_fail else ''}valid tests ({len(tests)})",
|
||
)
|
||
all_pass = all_pass and success
|
||
|
||
self.assertTrue(all_pass, "failed one or more numeric tests")
|
||
|
||
def testTokenMap(self):
|
||
parser = pp.OneOrMore(pp.Word(pp.hexnums)).set_parse_action(
|
||
pp.token_map(int, 16)
|
||
)
|
||
success, report = parser.run_tests(
|
||
"""
|
||
00 11 22 aa FF 0a 0d 1a
|
||
"""
|
||
)
|
||
|
||
self.assertRunTestResults(
|
||
(success, report),
|
||
[([0, 17, 34, 170, 255, 10, 13, 26], "token_map parse action failed")],
|
||
msg="failed to parse hex integers",
|
||
)
|
||
|
||
def testParseFile(self):
|
||
s = """
|
||
123 456 789
|
||
"""
|
||
from pathlib import Path
|
||
|
||
integer = ppc.integer
|
||
test_parser = integer[1, ...]
|
||
|
||
input_file_as_stringio = StringIO(s)
|
||
input_file_as_str = "tests/parsefiletest_input_file.txt"
|
||
input_file_as_path = Path(input_file_as_str)
|
||
|
||
expected_list = [int(i) for i in s.split()]
|
||
|
||
for input_file in (
|
||
input_file_as_stringio,
|
||
input_file_as_str,
|
||
input_file_as_path,
|
||
):
|
||
with self.subTest(input_file=input_file):
|
||
print(f"parse_file() called with {type(input_file).__name__}")
|
||
results = test_parser.parse_file(input_file)
|
||
print(results)
|
||
self.assertEqual(expected_list, results.as_list())
|
||
|
||
def testHTMLStripper(self):
|
||
sample = """
|
||
<html>
|
||
Here is some sample <i>HTML</i> text.
|
||
</html>
|
||
"""
|
||
read_everything = pp.original_text_for(pp.OneOrMore(pp.Word(pp.printables)))
|
||
read_everything.add_parse_action(ppc.strip_html_tags)
|
||
|
||
result = read_everything.parse_string(sample, parse_all=True)
|
||
self.assertEqual("Here is some sample HTML text.", result[0].strip())
|
||
|
||
def testExprSplitter(self):
|
||
expr = pp.Literal(";") + pp.Empty()
|
||
expr.ignore(pp.quoted_string)
|
||
expr.ignore(pp.python_style_comment)
|
||
|
||
sample = """
|
||
def main():
|
||
this_semi_does_nothing();
|
||
neither_does_this_but_there_are_spaces_afterward();
|
||
a = "a;b"; return a # this is a comment; it has a semicolon!
|
||
|
||
def b():
|
||
if False:
|
||
z=1000;b("; in quotes"); c=200;return z
|
||
return ';'
|
||
|
||
class Foo(object):
|
||
def bar(self):
|
||
'''a docstring; with a semicolon'''
|
||
a = 10; b = 11; c = 12
|
||
|
||
# this comment; has several; semicolons
|
||
if self.spam:
|
||
x = 12; return x # so; does; this; one
|
||
x = 15;;; y += x; return y
|
||
|
||
def baz(self):
|
||
return self.bar
|
||
"""
|
||
expected = [
|
||
[" this_semi_does_nothing()", ""],
|
||
[" neither_does_this_but_there_are_spaces_afterward()", ""],
|
||
[
|
||
' a = "a;b"',
|
||
"return a # this is a comment; it has a semicolon!",
|
||
],
|
||
[" z=1000", 'b("; in quotes")', "c=200", "return z"],
|
||
[" return ';'"],
|
||
[" '''a docstring; with a semicolon'''"],
|
||
[" a = 10", "b = 11", "c = 12"],
|
||
[" # this comment; has several; semicolons"],
|
||
[" x = 12", "return x # so; does; this; one"],
|
||
[" x = 15", "", "", "y += x", "return y"],
|
||
]
|
||
|
||
exp_iter = iter(expected)
|
||
for line in filter(lambda ll: ";" in ll, sample.splitlines()):
|
||
print(str(list(expr.split(line))) + ",")
|
||
self.assertEqual(
|
||
next(exp_iter), list(expr.split(line)), "invalid split on expression"
|
||
)
|
||
|
||
print()
|
||
|
||
expected = [
|
||
[" this_semi_does_nothing()", ";", ""],
|
||
[" neither_does_this_but_there_are_spaces_afterward()", ";", ""],
|
||
[
|
||
' a = "a;b"',
|
||
";",
|
||
"return a # this is a comment; it has a semicolon!",
|
||
],
|
||
[
|
||
" z=1000",
|
||
";",
|
||
'b("; in quotes")',
|
||
";",
|
||
"c=200",
|
||
";",
|
||
"return z",
|
||
],
|
||
[" return ';'"],
|
||
[" '''a docstring; with a semicolon'''"],
|
||
[" a = 10", ";", "b = 11", ";", "c = 12"],
|
||
[" # this comment; has several; semicolons"],
|
||
[" x = 12", ";", "return x # so; does; this; one"],
|
||
[
|
||
" x = 15",
|
||
";",
|
||
"",
|
||
";",
|
||
"",
|
||
";",
|
||
"y += x",
|
||
";",
|
||
"return y",
|
||
],
|
||
]
|
||
exp_iter = iter(expected)
|
||
for line in filter(lambda ll: ";" in ll, sample.splitlines()):
|
||
print(str(list(expr.split(line, include_separators=True))) + ",")
|
||
self.assertEqual(
|
||
next(exp_iter),
|
||
list(expr.split(line, include_separators=True)),
|
||
"invalid split on expression",
|
||
)
|
||
|
||
print()
|
||
|
||
expected = [
|
||
[" this_semi_does_nothing()", ""],
|
||
[" neither_does_this_but_there_are_spaces_afterward()", ""],
|
||
[
|
||
' a = "a;b"',
|
||
"return a # this is a comment; it has a semicolon!",
|
||
],
|
||
[" z=1000", 'b("; in quotes"); c=200;return z'],
|
||
[" a = 10", "b = 11; c = 12"],
|
||
[" x = 12", "return x # so; does; this; one"],
|
||
[" x = 15", ";; y += x; return y"],
|
||
]
|
||
exp_iter = iter(expected)
|
||
for line in sample.splitlines():
|
||
pieces = list(expr.split(line, maxsplit=1))
|
||
print(str(pieces) + ",")
|
||
if len(pieces) == 2:
|
||
exp = next(exp_iter)
|
||
self.assertEqual(
|
||
exp, pieces, "invalid split on expression with maxSplits=1"
|
||
)
|
||
elif len(pieces) == 1:
|
||
self.assertEqual(
|
||
0,
|
||
len(expr.search_string(line)),
|
||
"invalid split with maxSplits=1 when expr not present",
|
||
)
|
||
else:
|
||
print("\n>>> " + line)
|
||
self.fail("invalid split on expression with maxSplits=1, corner case")
|
||
|
||
def testParseFatalException(self):
|
||
with self.assertRaisesParseException(
|
||
exc_type=ParseFatalException, msg="failed to raise ErrorStop exception"
|
||
):
|
||
expr = "ZZZ" - pp.Word(pp.nums)
|
||
expr.parse_string("ZZZ bad", parse_all=True)
|
||
|
||
def testParseFatalException2(self):
|
||
# Fatal exception raised in MatchFirst should not be superseded later non-fatal exceptions
|
||
# addresses Issue #251
|
||
|
||
def raise_exception(tokens):
|
||
raise pp.ParseSyntaxException("should raise here")
|
||
|
||
test = pp.MatchFirst(
|
||
(
|
||
pp.pyparsing_common.integer + pp.pyparsing_common.identifier
|
||
).set_parse_action(raise_exception)
|
||
| pp.pyparsing_common.number
|
||
)
|
||
|
||
with self.assertRaisesParseException(pp.ParseFatalException):
|
||
test.parse_string("1s", parse_all=True)
|
||
|
||
def testParseFatalException3(self):
|
||
# Fatal exception raised in MatchFirst should not be superseded later non-fatal exceptions
|
||
# addresses Issue #251
|
||
|
||
test = pp.MatchFirst(
|
||
(pp.pyparsing_common.integer - pp.pyparsing_common.identifier)
|
||
| pp.pyparsing_common.integer
|
||
)
|
||
|
||
with self.assertRaisesParseException(pp.ParseFatalException):
|
||
test.parse_string("1", parse_all=True)
|
||
|
||
def testInlineLiteralsUsing(self):
|
||
wd = pp.Word(pp.alphas)
|
||
|
||
pp.ParserElement.inline_literals_using(pp.Suppress)
|
||
result = (wd + "," + wd + pp.one_of("! . ?")).parse_string(
|
||
"Hello, World!", parse_all=True
|
||
)
|
||
self.assertEqual(3, len(result), "inline_literals_using(Suppress) failed!")
|
||
|
||
pp.ParserElement.inline_literals_using(pp.Literal)
|
||
result = (wd + "," + wd + pp.one_of("! . ?")).parse_string(
|
||
"Hello, World!", parse_all=True
|
||
)
|
||
self.assertEqual(4, len(result), "inline_literals_using(Literal) failed!")
|
||
|
||
pp.ParserElement.inline_literals_using(pp.CaselessKeyword)
|
||
self.assertParseAndCheckList(
|
||
"SELECT" + wd + "FROM" + wd,
|
||
"select color from colors",
|
||
expected_list=["SELECT", "color", "FROM", "colors"],
|
||
msg="inline_literals_using(CaselessKeyword) failed!",
|
||
verbose=True,
|
||
)
|
||
|
||
pp.ParserElement.inline_literals_using(pp.CaselessLiteral)
|
||
self.assertParseAndCheckList(
|
||
"SELECT" + wd + "FROM" + wd,
|
||
"select color from colors",
|
||
expected_list=["SELECT", "color", "FROM", "colors"],
|
||
msg="inline_literals_using(CaselessLiteral) failed!",
|
||
verbose=True,
|
||
)
|
||
|
||
integer = pp.Word(pp.nums)
|
||
pp.ParserElement.inline_literals_using(pp.Literal)
|
||
date_str = integer("year") + "/" + integer("month") + "/" + integer("day")
|
||
self.assertParseAndCheckList(
|
||
date_str,
|
||
"1999/12/31",
|
||
expected_list=["1999", "/", "12", "/", "31"],
|
||
msg="inline_literals_using(example 1) failed!",
|
||
verbose=True,
|
||
)
|
||
|
||
# change to Suppress
|
||
pp.ParserElement.inline_literals_using(pp.Suppress)
|
||
date_str = integer("year") + "/" + integer("month") + "/" + integer("day")
|
||
|
||
self.assertParseAndCheckList(
|
||
date_str,
|
||
"1999/12/31",
|
||
expected_list=["1999", "12", "31"],
|
||
msg="inline_literals_using(example 2) failed!",
|
||
verbose=True,
|
||
)
|
||
|
||
def testCloseMatch(self):
|
||
searchseq = pp.CloseMatch("ATCATCGAATGGA", 2)
|
||
|
||
_, results = searchseq.run_tests(
|
||
"""
|
||
ATCATCGAATGGA
|
||
XTCATCGAATGGX
|
||
ATCATCGAAXGGA
|
||
ATCAXXGAATGGA
|
||
ATCAXXGAATGXA
|
||
ATCAXXGAATGG
|
||
"""
|
||
)
|
||
expected = ([], [0, 12], [9], [4, 5], None, None)
|
||
|
||
for r, exp in zip(results, expected):
|
||
if exp is not None:
|
||
self.assertEqual(
|
||
exp,
|
||
r[1].mismatches,
|
||
f"fail CloseMatch between {searchseq.match_string!r} and {r[0]!r}",
|
||
)
|
||
print(
|
||
r[0],
|
||
(
|
||
f"exc: {r[1]}"
|
||
if exp is None and isinstance(r[1], Exception)
|
||
else ("no match", "match")[r[1].mismatches == exp]
|
||
),
|
||
)
|
||
|
||
def testCloseMatchCaseless(self):
|
||
searchseq = pp.CloseMatch("ATCATCGAATGGA", 2, caseless=True)
|
||
|
||
_, results = searchseq.run_tests(
|
||
"""
|
||
atcatcgaatgga
|
||
xtcatcgaatggx
|
||
atcatcgaaxgga
|
||
atcaxxgaatgga
|
||
atcaxxgaatgxa
|
||
atcaxxgaatgg
|
||
"""
|
||
)
|
||
expected = ([], [0, 12], [9], [4, 5], None, None)
|
||
|
||
for r, exp in zip(results, expected):
|
||
if exp is not None:
|
||
self.assertEqual(
|
||
exp,
|
||
r[1].mismatches,
|
||
f"fail CaselessCloseMatch between {searchseq.match_string!r} and {r[0]!r}",
|
||
)
|
||
print(
|
||
r[0],
|
||
(
|
||
f"exc: {r[1]}"
|
||
if exp is None and isinstance(r[1], Exception)
|
||
else ("no match", "match")[r[1].mismatches == exp]
|
||
),
|
||
)
|
||
|
||
def testDefaultKeywordChars(self):
|
||
with self.assertRaisesParseException(
|
||
msg="failed to fail matching keyword using updated keyword chars"
|
||
):
|
||
pp.Keyword("start").parse_string("start1000", parse_all=True)
|
||
|
||
try:
|
||
pp.Keyword("start", ident_chars=pp.alphas).parse_string(
|
||
"start1000", parse_all=False
|
||
)
|
||
except pp.ParseException:
|
||
self.fail("failed to match keyword using updated keyword chars")
|
||
|
||
with ppt.reset_pyparsing_context():
|
||
pp.Keyword.set_default_keyword_chars(pp.alphas)
|
||
try:
|
||
pp.Keyword("start").parse_string("start1000", parse_all=False)
|
||
except pp.ParseException:
|
||
self.fail("failed to match keyword using updated keyword chars")
|
||
|
||
with self.assertRaisesParseException(
|
||
msg="failed to fail matching keyword using updated keyword chars"
|
||
):
|
||
pp.CaselessKeyword("START").parse_string("start1000", parse_all=False)
|
||
|
||
try:
|
||
pp.CaselessKeyword("START", ident_chars=pp.alphas).parse_string(
|
||
"start1000", parse_all=False
|
||
)
|
||
except pp.ParseException:
|
||
self.fail("failed to match keyword using updated keyword chars")
|
||
|
||
with ppt.reset_pyparsing_context():
|
||
pp.Keyword.set_default_keyword_chars(pp.alphas)
|
||
try:
|
||
pp.CaselessKeyword("START").parse_string("start1000", parse_all=False)
|
||
except pp.ParseException:
|
||
self.assertTrue(
|
||
False, "failed to match keyword using updated keyword chars"
|
||
)
|
||
|
||
def testKeywordCopyIdentChars(self):
|
||
a_keyword = pp.Keyword("start", ident_chars="_")
|
||
b_keyword = a_keyword.copy()
|
||
self.assertEqual(a_keyword.ident_chars, b_keyword.ident_chars)
|
||
|
||
def testCopyLiteralAttrs(self):
|
||
lit = pp.Literal("foo").leave_whitespace()
|
||
lit2 = lit.copy()
|
||
self.assertFalse(lit2.skipWhitespace)
|
||
lit3 = lit2.ignore_whitespace().copy()
|
||
self.assertTrue(lit3.skipWhitespace)
|
||
|
||
def testWordParseImpl(self):
|
||
ppc = pp.common
|
||
ppu = pp.unicode
|
||
ident_exprs = [
|
||
ppc.identifier,
|
||
pp.Word(pp.alphas),
|
||
pp.Literal("identifier"),
|
||
pp.Regex(r"[a-zA-Z_][a-zA-Z0-9_]*"),
|
||
pp.Regex(r"[a-zA-Z_][a-zA-Z0-9_]*", as_match=True),
|
||
pp.Regex(r"[a-zA-Z_][a-zA-Z0-9_]*", as_group_list=True),
|
||
ppu.BMP.identifier,
|
||
pp.Char(pp.alphas),
|
||
pp.Keyword("identifier"),
|
||
]
|
||
for ident in ident_exprs:
|
||
with self.subTest(msg=f"Using expression type {type(ident).__name__}"):
|
||
custom_id = (
|
||
ident()
|
||
.set_name("standard identifier")
|
||
.copy()
|
||
.set_name("custom identifier")
|
||
)
|
||
with self.assertRaisesParseException(
|
||
expected_msg="Expected custom identifier"
|
||
):
|
||
custom_id.parse_string("1", parse_all=True)
|
||
|
||
with self.subTest(msg="Using integer"):
|
||
custom_int = (
|
||
ppc.integer()
|
||
.set_name("standard integer")
|
||
.copy()
|
||
.set_name("custom integer")
|
||
)
|
||
with self.assertRaisesParseException(
|
||
expected_msg="Expected custom integer"
|
||
):
|
||
custom_int.parse_string("z", parse_all=True)
|
||
|
||
def testWordCopyWhenWordCharsIncludeSpace(self):
|
||
# Issue #618 - with copy()
|
||
word_with_space = pp.Word(pp.alphas + " ")
|
||
word_with_space.parse_string("ABC")
|
||
# no longer raises exception
|
||
word_with_space.copy().parse_string("ABC")
|
||
|
||
def testWordCopyWhenWordCharsIncludeSpace2(self):
|
||
# Issue #618 - with DelimitedList()
|
||
element = pp.QuotedString('"') | pp.Combine(
|
||
pp.Word(" abcdefghijklmnopqrstuvwxyz")
|
||
)
|
||
element.parse_string("abc")
|
||
element_list = pp.DelimitedList(element)
|
||
# no longer raises exception
|
||
element_list.parse_string("abc")
|
||
|
||
def testWordCopyWhenWordCharsIncludeSpace3(self):
|
||
# Issue #618 - with results name
|
||
word_with_space = pp.Word(pp.alphas + " ")
|
||
word_with_space.parse_string("ABC")
|
||
# no longer raises exception
|
||
word_with_space("trouble").parse_string("ABC")
|
||
|
||
def testLiteralVsKeyword(self):
|
||
integer = ppc.integer
|
||
literal_expr = integer + pp.Literal("start") + integer
|
||
keyword_expr = integer + pp.Keyword("start") + integer
|
||
caseless_keyword_expr = integer + pp.CaselessKeyword("START") + integer
|
||
word_keyword_expr = (
|
||
integer + pp.Word(pp.alphas, as_keyword=True).set_name("word") + integer
|
||
)
|
||
|
||
print()
|
||
test_string = "1 start 2"
|
||
print(test_string)
|
||
print(literal_expr, literal_expr.parse_string(test_string, parse_all=True))
|
||
print(keyword_expr, keyword_expr.parse_string(test_string, parse_all=True))
|
||
print(
|
||
caseless_keyword_expr,
|
||
caseless_keyword_expr.parse_string(test_string, parse_all=True),
|
||
)
|
||
print(
|
||
word_keyword_expr,
|
||
word_keyword_expr.parse_string(test_string, parse_all=True),
|
||
)
|
||
print()
|
||
|
||
test_string = "3 start4"
|
||
print(test_string)
|
||
print(literal_expr, literal_expr.parse_string(test_string, parse_all=True))
|
||
with self.assertRaisesParseException(
|
||
msg="failed to fail matching keyword using updated keyword chars"
|
||
):
|
||
print(keyword_expr.parse_string(test_string, parse_all=True))
|
||
|
||
with self.assertRaisesParseException(
|
||
msg="failed to fail matching keyword using updated keyword chars"
|
||
):
|
||
print(caseless_keyword_expr.parse_string(test_string, parse_all=True))
|
||
|
||
with self.assertRaisesParseException(
|
||
msg="failed to fail matching keyword using updated keyword chars"
|
||
):
|
||
print(word_keyword_expr.parse_string(test_string, parse_all=True))
|
||
print()
|
||
|
||
test_string = "5start 6"
|
||
print(test_string)
|
||
print(literal_expr.parse_string(test_string, parse_all=True))
|
||
with self.assertRaisesParseException(
|
||
msg="failed to fail matching keyword using updated keyword chars"
|
||
):
|
||
print(keyword_expr.parse_string(test_string, parse_all=True))
|
||
|
||
with self.assertRaisesParseException(
|
||
msg="failed to fail matching keyword using updated keyword chars"
|
||
):
|
||
print(caseless_keyword_expr.parse_string(test_string, parse_all=True))
|
||
|
||
with self.assertRaisesParseException(
|
||
msg="failed to fail matching keyword using updated keyword chars"
|
||
):
|
||
print(word_keyword_expr.parse_string(test_string, parse_all=True))
|
||
|
||
def testCol(self):
|
||
test = "*\n* \n* ALF\n*\n"
|
||
initials = [c for i, c in enumerate(test) if pp.col(i, test) == 1]
|
||
print(initials)
|
||
self.assertTrue(
|
||
len(initials) == 4 and all(c == "*" for c in initials), "fail col test"
|
||
)
|
||
|
||
def testLiteralException(self):
|
||
for cls in (
|
||
pp.Literal,
|
||
pp.CaselessLiteral,
|
||
pp.Keyword,
|
||
pp.CaselessKeyword,
|
||
pp.Word,
|
||
pp.Regex,
|
||
):
|
||
expr = cls("xyz") # .set_name('{}_expr'.format(cls.__name__.lower()))
|
||
|
||
try:
|
||
expr.parse_string(" ", parse_all=True)
|
||
except Exception as e:
|
||
print(cls.__name__, str(e))
|
||
self.assertTrue(
|
||
isinstance(e, pp.ParseBaseException),
|
||
f"class {cls.__name__} raised wrong exception type {type(e).__name__}",
|
||
)
|
||
|
||
def testParseActionIndexErrorException(self):
|
||
"""
|
||
Tests raising an IndexError in a parse action
|
||
"""
|
||
import traceback
|
||
|
||
number = pp.Word(pp.nums)
|
||
|
||
def number_action():
|
||
raise IndexError # this is the important line!
|
||
|
||
number.add_parse_action(number_action)
|
||
symbol = pp.Word("abcd", max=1)
|
||
expr = pp.Group(number) ^ symbol
|
||
|
||
try:
|
||
expr.parse_string("1 + 2", parse_all=True)
|
||
except IndexError as ie:
|
||
pass
|
||
except Exception as e:
|
||
traceback.print_exc()
|
||
self.fail(f"Expected IndexError not raised, raised {type(e).__name__}: {e}")
|
||
else:
|
||
self.fail("Expected IndexError not raised")
|
||
|
||
# tests Issue #22
|
||
def testParseActionNesting(self):
|
||
vals = pp.OneOrMore(ppc.integer)("int_values")
|
||
|
||
def add_total(tokens):
|
||
tokens["total"] = sum(tokens)
|
||
return tokens
|
||
|
||
vals.add_parse_action(add_total)
|
||
results = vals.parse_string("244 23 13 2343", parse_all=True)
|
||
print(results.dump())
|
||
self.assertParseResultsEquals(
|
||
results,
|
||
expected_dict={"int_values": [244, 23, 13, 2343], "total": 2623},
|
||
msg="noop parse action changed ParseResults structure",
|
||
)
|
||
|
||
name = pp.Word(pp.alphas)("name")
|
||
score = pp.Word(pp.nums + ".")("score")
|
||
nameScore = pp.Group(name + score)
|
||
line1 = nameScore("Rider")
|
||
|
||
result1 = line1.parse_string("Mauney 46.5", parse_all=True)
|
||
|
||
print("### before parse action is added ###")
|
||
print("result1.dump():\n" + result1.dump() + "\n")
|
||
before_pa_dict = result1.as_dict()
|
||
|
||
line1.set_parse_action(lambda t: t)
|
||
|
||
result1 = line1.parse_string("Mauney 46.5", parse_all=True)
|
||
after_pa_dict = result1.as_dict()
|
||
|
||
print("### after parse action was added ###")
|
||
print("result1.dump():\n" + result1.dump() + "\n")
|
||
self.assertEqual(
|
||
before_pa_dict,
|
||
after_pa_dict,
|
||
"noop parse action changed ParseResults structure",
|
||
)
|
||
|
||
def testParseActionWithDelimitedList(self):
|
||
class AnnotatedToken:
|
||
def __init__(self, kind, elements):
|
||
self.kind = kind
|
||
self.elements = elements
|
||
|
||
def __str__(self):
|
||
return f"AnnotatedToken({self.kind!r}, {self.elements!r})"
|
||
|
||
def __eq__(self, other):
|
||
return (
|
||
type(self) == type(other)
|
||
and self.kind == other.kind
|
||
and self.elements == other.elements
|
||
)
|
||
|
||
__repr__ = __str__
|
||
|
||
def annotate(name):
|
||
def _(t):
|
||
return AnnotatedToken(name, t.as_list())
|
||
|
||
return _
|
||
|
||
identifier = pp.Word(pp.srange("[a-z0-9]"))
|
||
numeral = pp.Word(pp.nums)
|
||
|
||
named_number_value = pp.Suppress("(") + numeral + pp.Suppress(")")
|
||
named_number = identifier + named_number_value
|
||
|
||
named_number_list = (
|
||
pp.Suppress("{")
|
||
+ pp.Group(pp.Optional(pp.DelimitedList(named_number)))
|
||
+ pp.Suppress("}")
|
||
)
|
||
|
||
# repro but in #345 - DelimitedList silently changes contents of named_number
|
||
named_number_value.set_parse_action(annotate("val"))
|
||
|
||
test_string = "{ x1(1), x2(2) }"
|
||
expected = [
|
||
["x1", AnnotatedToken("val", ["1"]), "x2", AnnotatedToken("val", ["2"])]
|
||
]
|
||
|
||
self.assertParseAndCheckList(named_number_list, test_string, expected)
|
||
|
||
def testParseActionRunsInNotAny(self):
|
||
# see Issue #482
|
||
data = """ [gog1] [G1] [gog2] [gog3] [gog4] [G2] [gog5] [G3] [gog6] """
|
||
|
||
poi_type = pp.Word(pp.alphas).set_results_name("type")
|
||
poi = pp.Suppress("[") + poi_type + pp.Char(pp.nums) + pp.Suppress("]")
|
||
|
||
def cnd_is_type(val):
|
||
return lambda toks: toks.type == val
|
||
|
||
poi_gog = poi("gog").add_condition(cnd_is_type("gog"))
|
||
poi_g = poi("g").add_condition(cnd_is_type("G"))
|
||
|
||
pattern = poi_gog + ~poi_g
|
||
|
||
matches = pattern.search_string(data).as_list()
|
||
self.assertEqual(
|
||
[["gog", "2"], ["gog", "3"], ["gog", "6"]],
|
||
matches,
|
||
"failed testing parse actions being run inside a NotAny",
|
||
)
|
||
|
||
def testParseResultsNameBelowUngroupedName(self):
|
||
rule_num = pp.Regex("[0-9]+")("LIT_NUM*")
|
||
list_num = pp.Group(
|
||
pp.Literal("[")("START_LIST")
|
||
+ pp.DelimitedList(rule_num)("LIST_VALUES")
|
||
+ pp.Literal("]")("END_LIST")
|
||
)("LIST")
|
||
|
||
test_string = "[ 1,2,3,4,5,6 ]"
|
||
success, _ = list_num.run_tests(test_string)
|
||
self.assertTrue(success)
|
||
|
||
U = list_num.parse_string(test_string, parse_all=True)
|
||
self.assertTrue(
|
||
"LIT_NUM" not in U.LIST.LIST_VALUES,
|
||
"results name retained as sub in ungrouped named result",
|
||
)
|
||
|
||
def testParseResultsNamesInGroupWithDict(self):
|
||
key = ppc.identifier()
|
||
value = ppc.integer()
|
||
lat = ppc.real()
|
||
long = ppc.real()
|
||
EQ = pp.Suppress("=")
|
||
|
||
data = (
|
||
lat("lat")
|
||
+ long("long")
|
||
+ pp.Dict(pp.OneOrMore(pp.Group(key + EQ + value)))
|
||
)
|
||
site = pp.QuotedString('"')("name") + pp.Group(data)("data")
|
||
|
||
test_string = '"Golden Gate Bridge" 37.819722 -122.478611 height=746 span=4200'
|
||
success, _ = site.run_tests(test_string)
|
||
self.assertTrue(success)
|
||
|
||
a, aEnd = pp.make_html_tags("a")
|
||
attrs = a.parse_string("<a href='blah'>", parse_all=True)
|
||
print(attrs.dump())
|
||
self.assertParseResultsEquals(
|
||
attrs,
|
||
expected_dict={
|
||
"startA": {"href": "blah", "tag": "a", "empty": False},
|
||
"href": "blah",
|
||
"tag": "a",
|
||
"empty": False,
|
||
},
|
||
)
|
||
|
||
def testMakeXMLTags(self):
|
||
"""test helper function make_xml_tags in simple use case"""
|
||
|
||
body, bodyEnd = pp.make_xml_tags("body")
|
||
tst = "<body>Hello</body>"
|
||
expr = body + pp.Word(pp.alphas)("contents") + bodyEnd
|
||
result = expr.parse_string(tst, parse_all=True)
|
||
print(result.dump())
|
||
self.assertParseResultsEquals(
|
||
result, ["body", False, "Hello", "</body>"], msg="issue using make_xml_tags"
|
||
)
|
||
|
||
def testFollowedBy(self):
|
||
expr = pp.Word(pp.alphas)("item") + pp.FollowedBy(ppc.integer("qty"))
|
||
result = expr.parse_string("balloon 99", parse_all=False)
|
||
print(result.dump())
|
||
self.assertTrue("qty" in result, "failed to capture results name in FollowedBy")
|
||
self.assertEqual(
|
||
{"item": "balloon", "qty": 99},
|
||
result.as_dict(),
|
||
"invalid results name structure from FollowedBy",
|
||
)
|
||
|
||
def testSetBreak(self):
|
||
"""
|
||
Test behavior of ParserElement.set_break(), to invoke the debugger before parsing that element is attempted.
|
||
|
||
Temporarily monkeypatches sys.breakpointhook().
|
||
"""
|
||
was_called = False
|
||
|
||
def mock_set_trace(*args, **kwargs):
|
||
nonlocal was_called
|
||
was_called = True
|
||
|
||
wd = pp.Word(pp.alphas)
|
||
wd.set_break()
|
||
|
||
print("Before parsing with set_break:", was_called)
|
||
|
||
with ppt.reset_pyparsing_context():
|
||
sys.breakpointhook = mock_set_trace
|
||
wd.parse_string("ABC", parse_all=True)
|
||
|
||
print("After parsing with set_break:", was_called)
|
||
sys.breakpointhook = sys.__breakpointhook__
|
||
self.assertTrue(was_called, "set_trace wasn't called by set_break")
|
||
|
||
def testUnicodeTests(self):
|
||
import unicodedata
|
||
|
||
ppu = pp.pyparsing_unicode
|
||
|
||
unicode_version = unicodedata.unidata_version
|
||
print(f"Unicode version {unicode_version}")
|
||
|
||
# verify ranges are converted to sets properly
|
||
for unicode_property, expected_length in [
|
||
("alphas", 48965),
|
||
("alphanums", 49430),
|
||
("identchars", 49013),
|
||
("identbodychars", 50729),
|
||
("printables", 65484),
|
||
]:
|
||
charset = getattr(ppu.BMP, unicode_property)
|
||
charset_len = len(charset)
|
||
|
||
# this subtest is sensitive to the Unicode version used in the current
|
||
# python version
|
||
if unicode_version == "14.0.0":
|
||
with self.subTest(unicode_property=unicode_property, msg="verify len"):
|
||
print(f"ppu.BMP.{unicode_property:14}: {charset_len:6d}")
|
||
self.assertEqual(
|
||
charset_len,
|
||
expected_length,
|
||
f"incorrect number of ppu.BMP.{unicode_property},"
|
||
f" found {charset_len} expected {expected_length}",
|
||
)
|
||
|
||
with self.subTest(unicode_property=unicode_property, msg="verify unique"):
|
||
char_counts = collections.Counter(charset)
|
||
self.assertTrue(
|
||
all(count == 1 for count in char_counts.values()),
|
||
f"duplicate items found in ppu.BMP.{unicode_property}:"
|
||
f" {[(ord(c), c) for c, count in char_counts.items() if count > 1]}",
|
||
)
|
||
|
||
# verify proper merging of ranges by addition
|
||
kanji_printables = ppu.Japanese.Kanji.printables
|
||
katakana_printables = ppu.Japanese.Katakana.printables
|
||
hiragana_printables = ppu.Japanese.Hiragana.printables
|
||
japanese_printables = ppu.Japanese.printables
|
||
with self.subTest(msg="verify constructing ranges by merging types"):
|
||
self.assertEqual(
|
||
set(kanji_printables + katakana_printables + hiragana_printables),
|
||
set(japanese_printables),
|
||
"failed to construct ranges by merging Japanese types",
|
||
)
|
||
|
||
# verify proper merging of ranges using multiple inheritance
|
||
cjk_printables = ppu.CJK.printables
|
||
chinese_printables = ppu.Chinese.printables
|
||
korean_printables = ppu.Korean.printables
|
||
with self.subTest(
|
||
msg="verify merging ranges by using multiple inheritance generates unique list of characters"
|
||
):
|
||
char_counts = collections.Counter(cjk_printables)
|
||
self.assertTrue(
|
||
all(count == 1 for count in char_counts.values()),
|
||
"duplicate items found in ppu.CJK.printables:"
|
||
f" {[(ord(c), c) for c, count in char_counts.items() if count > 1]}",
|
||
)
|
||
|
||
with self.subTest(
|
||
msg="verify merging ranges by using multiple inheritance generates sorted list of characters"
|
||
):
|
||
self.assertEqual(
|
||
list(cjk_printables),
|
||
sorted(cjk_printables),
|
||
"CJK printables are not sorted",
|
||
)
|
||
|
||
with self.subTest(
|
||
msg="verify summing chars is equivalent to merging ranges by using multiple inheritance (CJK)"
|
||
):
|
||
print(
|
||
len(set(chinese_printables + korean_printables + japanese_printables)),
|
||
len(cjk_printables),
|
||
)
|
||
|
||
self.assertEqual(
|
||
set(chinese_printables + korean_printables + japanese_printables),
|
||
set(cjk_printables),
|
||
"failed to construct ranges by merging Chinese, Japanese and Korean",
|
||
)
|
||
|
||
def testUnicodeTests2(self):
|
||
ppu = pp.unicode
|
||
|
||
alphas = ppu.Greek.alphas
|
||
greet = pp.Word(alphas) + "," + pp.Word(alphas) + "!"
|
||
|
||
# input string
|
||
hello = "Καλημέρα, κόσμε!"
|
||
result = greet.parse_string(hello, parse_all=True)
|
||
print(result)
|
||
self.assertParseResultsEquals(
|
||
result,
|
||
expected_list=["Καλημέρα", ",", "κόσμε", "!"],
|
||
msg="Failed to parse Greek 'Hello, World!' using "
|
||
"pyparsing_unicode.Greek.alphas",
|
||
)
|
||
|
||
# define a custom unicode range using multiple inheritance
|
||
class Turkish_set(ppu.Latin1, ppu.LatinA):
|
||
pass
|
||
|
||
for attrname in "printables alphas nums identchars identbodychars".split():
|
||
with self.subTest(
|
||
"verify unicode_set composed using MI", attrname=attrname
|
||
):
|
||
latin1_value = getattr(ppu.Latin1, attrname)
|
||
latinA_value = getattr(ppu.LatinA, attrname)
|
||
turkish_value = getattr(Turkish_set, attrname)
|
||
self.assertEqual(
|
||
set(latin1_value + latinA_value),
|
||
set(turkish_value),
|
||
f"failed to construct ranges by merging Latin1 and LatinA ({attrname})",
|
||
)
|
||
|
||
with self.subTest("Test using new Turkish_set for parsing"):
|
||
key = pp.Word(Turkish_set.alphas)
|
||
value = ppc.integer | pp.Word(Turkish_set.alphas, Turkish_set.alphanums)
|
||
EQ = pp.Suppress("=")
|
||
key_value = key + EQ + value
|
||
|
||
sample = """\
|
||
şehir=İzmir
|
||
ülke=Türkiye
|
||
nüfus=4279677"""
|
||
result = pp.Dict(pp.OneOrMore(pp.Group(key_value))).parse_string(
|
||
sample, parse_all=True
|
||
)
|
||
|
||
print(result.dump())
|
||
self.assertParseResultsEquals(
|
||
result,
|
||
expected_dict={"şehir": "İzmir", "ülke": "Türkiye", "nüfus": 4279677},
|
||
msg="Failed to parse Turkish key-value pairs",
|
||
)
|
||
|
||
# Basic Multilingual Plane only contains chars up to 65535
|
||
def filter_16_bit(s):
|
||
return "".join(c for c in s if ord(c) < 2**16)
|
||
|
||
with self.subTest():
|
||
bmp_printables = ppu.BMP.printables
|
||
sample = (
|
||
"".join(
|
||
random.choice(filter_16_bit(unicode_set.printables))
|
||
for unicode_set in (
|
||
ppu.Japanese,
|
||
Turkish_set,
|
||
ppu.Greek,
|
||
ppu.Hebrew,
|
||
ppu.Devanagari,
|
||
ppu.Hangul,
|
||
ppu.Latin1,
|
||
ppu.Chinese,
|
||
ppu.Cyrillic,
|
||
ppu.Arabic,
|
||
ppu.Thai,
|
||
)
|
||
for _ in range(8)
|
||
)
|
||
+ "\N{REPLACEMENT CHARACTER}"
|
||
)
|
||
print(sample)
|
||
self.assertParseAndCheckList(pp.Word(bmp_printables), sample, [sample])
|
||
|
||
def testUnicodeSetNameEquivalence(self):
|
||
ppu = pp.unicode
|
||
|
||
for ascii_name, unicode_name in [
|
||
("Arabic", "العربية"),
|
||
("Chinese", "中文"),
|
||
("Cyrillic", "кириллица"),
|
||
("Greek", "Ελληνικά"),
|
||
("Hebrew", "עִברִית"),
|
||
("Japanese", "日本語"),
|
||
("Korean", "한국어"),
|
||
("Thai", "ไทย"),
|
||
("Devanagari", "देवनागरी"),
|
||
]:
|
||
with self.subTest(ascii_name=ascii_name, unicode_name=unicode_name):
|
||
self.assertTrue(
|
||
eval(f"ppu.{ascii_name} is ppu.{unicode_name}", {}, locals())
|
||
)
|
||
|
||
def testIndentedBlock(self):
|
||
# parse pseudo-yaml indented text
|
||
|
||
EQ = pp.Suppress("=")
|
||
stack = [1]
|
||
key = ppc.identifier
|
||
value = pp.Forward()
|
||
key_value = pp.Group(key + EQ + value)
|
||
compound_value = pp.Dict(pp.ungroup(pp.IndentedBlock(key_value, grouped=True)))
|
||
value <<= ppc.integer | pp.QuotedString("'") | compound_value
|
||
parser = pp.Dict(pp.OneOrMore(key_value))
|
||
|
||
text = """
|
||
a = 100
|
||
b = 101
|
||
c =
|
||
c1 = 200
|
||
c2 =
|
||
c21 = 999
|
||
c3 = 'A horse, a horse, my kingdom for a horse'
|
||
d = 505
|
||
"""
|
||
text = dedent(text)
|
||
print(text)
|
||
|
||
result = parser.parse_string(text, parse_all=True)
|
||
print(result.dump())
|
||
self.assertEqual(100, result.a, "invalid indented block result")
|
||
self.assertEqual(200, result.c.c1, "invalid indented block result")
|
||
self.assertEqual(999, result.c.c2.c21, "invalid indented block result")
|
||
|
||
# exercise indented_block with example posted in issue #87
|
||
def testIndentedBlockTest2(self):
|
||
indent_stack = [1]
|
||
|
||
key = pp.Word(pp.alphas, pp.alphanums) + pp.Suppress(":")
|
||
stmt = pp.Forward()
|
||
|
||
suite = pp.IndentedBlock(stmt, grouped=True)
|
||
body = key + suite
|
||
|
||
pattern = (
|
||
pp.Word(pp.alphas)
|
||
+ pp.Suppress("(")
|
||
+ pp.Word(pp.alphas)
|
||
+ pp.Suppress(")")
|
||
)
|
||
stmt <<= pattern
|
||
|
||
def key_parse_action(toks):
|
||
print(f"Parsing '{toks[0]}'...")
|
||
|
||
key.set_parse_action(key_parse_action)
|
||
header = pp.Suppress("[") + pp.Literal("test") + pp.Suppress("]")
|
||
content = header - pp.OneOrMore(pp.IndentedBlock(body))
|
||
|
||
contents = pp.Forward()
|
||
suites = pp.IndentedBlock(content)
|
||
|
||
extra = pp.Literal("extra") + pp.Suppress(":") - suites
|
||
contents <<= content | extra
|
||
|
||
parser = pp.OneOrMore(contents)
|
||
|
||
sample = dedent(
|
||
"""
|
||
extra:
|
||
[test]
|
||
one0:
|
||
two (three)
|
||
four0:
|
||
five (seven)
|
||
extra:
|
||
[test]
|
||
one1:
|
||
two (three)
|
||
four1:
|
||
five (seven)
|
||
"""
|
||
)
|
||
|
||
success, _ = parser.run_tests([sample])
|
||
self.assertTrue(success, "Failed IndentedBlock test for issue #87")
|
||
|
||
sample2 = dedent(
|
||
"""
|
||
extra:
|
||
[test]
|
||
one:
|
||
two (three)
|
||
four:
|
||
five (seven)
|
||
extra:
|
||
[test]
|
||
one:
|
||
two (three)
|
||
four:
|
||
five (seven)
|
||
|
||
[test]
|
||
one:
|
||
two (three)
|
||
four:
|
||
five (seven)
|
||
|
||
[test]
|
||
eight:
|
||
nine (ten)
|
||
eleven:
|
||
twelve (thirteen)
|
||
|
||
fourteen:
|
||
fifteen (sixteen)
|
||
seventeen:
|
||
eighteen (nineteen)
|
||
"""
|
||
)
|
||
|
||
del indent_stack[1:]
|
||
success, _ = parser.run_tests([sample2])
|
||
self.assertTrue(success, "Failed indented_block multi-block test for issue #87")
|
||
|
||
def testIndentedBlockScan(self):
|
||
def get_parser():
|
||
"""
|
||
A valid statement is the word "block:", followed by an indent, followed by the letter A only, or another block
|
||
"""
|
||
stack = [1]
|
||
block = pp.Forward()
|
||
body = pp.IndentedBlock(pp.Literal("A") ^ block)
|
||
block <<= pp.Literal("block:") + body
|
||
return block
|
||
|
||
# This input string is a perfect match for the parser, so a single match is found
|
||
p1 = get_parser()
|
||
r1 = list(
|
||
p1.scan_string(
|
||
dedent(
|
||
"""\
|
||
block:
|
||
A
|
||
"""
|
||
)
|
||
)
|
||
)
|
||
self.assertEqual(1, len(r1))
|
||
|
||
# This input string is a perfect match for the parser, except for the letter B instead of A, so this will fail (and should)
|
||
p2 = get_parser()
|
||
r2 = list(
|
||
p2.scan_string(
|
||
dedent(
|
||
"""\
|
||
block:
|
||
B
|
||
"""
|
||
)
|
||
)
|
||
)
|
||
self.assertEqual(0, len(r2))
|
||
|
||
# This input string contains both string A and string B, and it finds one match (as it should)
|
||
p3 = get_parser()
|
||
r3 = list(
|
||
p3.scan_string(
|
||
dedent(
|
||
"""\
|
||
block:
|
||
A
|
||
block:
|
||
B
|
||
"""
|
||
)
|
||
)
|
||
)
|
||
self.assertEqual(1, len(r3))
|
||
|
||
# This input string contains both string A and string B, but in a different order.
|
||
p4 = get_parser()
|
||
r4 = list(
|
||
p4.scan_string(
|
||
dedent(
|
||
"""\
|
||
block:
|
||
B
|
||
block:
|
||
A
|
||
"""
|
||
)
|
||
)
|
||
)
|
||
self.assertEqual(1, len(r4))
|
||
|
||
# This is the same as case 3, but with nesting
|
||
p5 = get_parser()
|
||
r5 = list(
|
||
p5.scan_string(
|
||
dedent(
|
||
"""\
|
||
block:
|
||
block:
|
||
A
|
||
block:
|
||
block:
|
||
B
|
||
"""
|
||
)
|
||
)
|
||
)
|
||
self.assertEqual(1, len(r5))
|
||
|
||
# This is the same as case 4, but with nesting
|
||
p6 = get_parser()
|
||
r6 = list(
|
||
p6.scan_string(
|
||
dedent(
|
||
"""\
|
||
block:
|
||
block:
|
||
B
|
||
block:
|
||
block:
|
||
A
|
||
"""
|
||
)
|
||
)
|
||
)
|
||
self.assertEqual(1, len(r6))
|
||
|
||
def testIndentedBlockClass(self):
|
||
data = """\
|
||
A
|
||
100
|
||
101
|
||
|
||
102
|
||
B
|
||
200
|
||
201
|
||
|
||
C
|
||
300
|
||
|
||
"""
|
||
|
||
integer = ppc.integer
|
||
group = pp.Group(pp.Char(pp.alphas) + pp.IndentedBlock(integer))
|
||
|
||
group[...].parse_string(data, parse_all=True).pprint()
|
||
|
||
self.assertParseAndCheckList(
|
||
group[...], data, [["A", [100, 101, 102]], ["B", [200, 201]], ["C", [300]]]
|
||
)
|
||
|
||
def testIndentedBlockClass2(self):
|
||
datas = [
|
||
"""\
|
||
A
|
||
100
|
||
B
|
||
200
|
||
201
|
||
""",
|
||
"""\
|
||
A
|
||
100
|
||
B
|
||
200
|
||
201
|
||
""",
|
||
"""\
|
||
A
|
||
100
|
||
B
|
||
200
|
||
201
|
||
""",
|
||
]
|
||
integer = ppc.integer
|
||
group = pp.Group(
|
||
pp.Char(pp.alphas) + pp.IndentedBlock(integer, recursive=False)
|
||
)
|
||
|
||
for data in datas:
|
||
print()
|
||
print(ppt.with_line_numbers(data))
|
||
|
||
print(group[...].parse_string(data).as_list())
|
||
self.assertParseAndCheckList(
|
||
group[...] + integer.suppress(),
|
||
data,
|
||
[["A", [100]], ["B", [200]]],
|
||
verbose=False,
|
||
)
|
||
|
||
def testIndentedBlockClassWithRecursion(self):
|
||
data = """\
|
||
|
||
A
|
||
100
|
||
101
|
||
|
||
102
|
||
B
|
||
b
|
||
200
|
||
201
|
||
|
||
C
|
||
300
|
||
|
||
"""
|
||
|
||
integer = ppc.integer
|
||
group = pp.Forward()
|
||
group <<= pp.Group(pp.Char(pp.alphas) + pp.IndentedBlock(integer | group))
|
||
|
||
print("using search_string")
|
||
print(group.search_string(data))
|
||
# print(sum(group.search_string(data)).dump())
|
||
|
||
self.assertParseAndCheckList(
|
||
group[...],
|
||
data,
|
||
[["A", [100, 101, 102]], ["B", [["b", [200, 201]]]], ["C", [300]]],
|
||
)
|
||
|
||
print("using parse_string")
|
||
print(group[...].parse_string(data, parse_all=True).dump())
|
||
|
||
dotted_int = pp.DelimitedList(
|
||
pp.Word(pp.nums), ".", allow_trailing_delim=True, combine=True
|
||
)
|
||
indented_expr = pp.IndentedBlock(dotted_int, recursive=True, grouped=True)
|
||
# indented_expr = pp.Forward()
|
||
# indented_expr <<= pp.IndentedBlock(dotted_int + indented_expr))
|
||
good_data = """\
|
||
1.
|
||
1.1
|
||
1.1.1
|
||
1.1.2
|
||
2."""
|
||
bad_data1 = """\
|
||
1.
|
||
1.1
|
||
1.1.1
|
||
1.2
|
||
2."""
|
||
bad_data2 = """\
|
||
1.
|
||
1.1
|
||
1.1.1
|
||
1.2
|
||
2."""
|
||
print("test good indentation")
|
||
print(pp.pyparsing_test.with_line_numbers(good_data))
|
||
print(indented_expr.parse_string(good_data, parse_all=True).as_list())
|
||
print()
|
||
|
||
print("test bad indentation")
|
||
print(pp.pyparsing_test.with_line_numbers(bad_data1))
|
||
with self.assertRaisesParseException(
|
||
msg="Failed to raise exception with bad indentation 1"
|
||
):
|
||
indented_expr.parse_string(bad_data1, parse_all=True)
|
||
|
||
print(pp.pyparsing_test.with_line_numbers(bad_data2))
|
||
with self.assertRaisesParseException(
|
||
msg="Failed to raise exception with bad indentation 2"
|
||
):
|
||
indented_expr.parse_string(bad_data2, parse_all=True)
|
||
|
||
def testInvalidDiagSetting(self):
|
||
with self.assertRaises(
|
||
ValueError,
|
||
msg="failed to raise exception when setting non-existent __diag__",
|
||
):
|
||
pp.__diag__.enable("xyzzy")
|
||
|
||
with self.assertWarns(
|
||
UserWarning, msg="failed to warn disabling 'collect_all_And_tokens"
|
||
):
|
||
pp.__compat__.disable("collect_all_And_tokens")
|
||
|
||
def testParseResultsWithNameMatchFirst(self):
|
||
expr_a = pp.Literal("not") + pp.Literal("the") + pp.Literal("bird")
|
||
expr_b = pp.Literal("the") + pp.Literal("bird")
|
||
expr = (expr_a | expr_b)("rexp")
|
||
|
||
success, report = expr.run_tests(
|
||
"""\
|
||
not the bird
|
||
the bird
|
||
"""
|
||
)
|
||
results = [rpt[1] for rpt in report]
|
||
self.assertParseResultsEquals(
|
||
results[0], ["not", "the", "bird"], {"rexp": ["not", "the", "bird"]}
|
||
)
|
||
self.assertParseResultsEquals(
|
||
results[1], ["the", "bird"], {"rexp": ["the", "bird"]}
|
||
)
|
||
|
||
# test compatibility mode, no longer restoring pre-2.3.1 behavior
|
||
with ppt.reset_pyparsing_context():
|
||
pp.__compat__.collect_all_And_tokens = False
|
||
pp.enable_diag(pp.Diagnostics.warn_multiple_tokens_in_named_alternation)
|
||
expr_a = pp.Literal("not") + pp.Literal("the") + pp.Literal("bird")
|
||
expr_b = pp.Literal("the") + pp.Literal("bird")
|
||
with self.assertWarns(
|
||
UserWarning, msg="failed to warn of And within alternation"
|
||
):
|
||
expr = (expr_a | expr_b)("rexp")
|
||
|
||
with self.assertDoesNotWarn(
|
||
UserWarning,
|
||
msg="warned when And within alternation warning was suppressed",
|
||
):
|
||
expr = (expr_a | expr_b).suppress_warning(
|
||
pp.Diagnostics.warn_multiple_tokens_in_named_alternation
|
||
)("rexp")
|
||
|
||
success, report = expr.run_tests(
|
||
"""
|
||
not the bird
|
||
the bird
|
||
"""
|
||
)
|
||
results = [rpt[1] for rpt in report]
|
||
self.assertParseResultsEquals(
|
||
results[0], ["not", "the", "bird"], {"rexp": ["not", "the", "bird"]}
|
||
)
|
||
self.assertParseResultsEquals(
|
||
results[1], ["the", "bird"], {"rexp": ["the", "bird"]}
|
||
)
|
||
|
||
def testParseResultsWithNameOr(self):
|
||
expr_a = pp.Literal("not") + pp.Literal("the") + pp.Literal("bird")
|
||
expr_b = pp.Literal("the") + pp.Literal("bird")
|
||
expr = (expr_a ^ expr_b)("rexp")
|
||
success, _ = expr.run_tests(
|
||
"""\
|
||
not the bird
|
||
the bird
|
||
"""
|
||
)
|
||
self.assertTrue(success)
|
||
|
||
result = expr.parse_string("not the bird", parse_all=True)
|
||
self.assertParseResultsEquals(
|
||
result, ["not", "the", "bird"], {"rexp": ["not", "the", "bird"]}
|
||
)
|
||
result = expr.parse_string("the bird", parse_all=True)
|
||
self.assertParseResultsEquals(
|
||
result, ["the", "bird"], {"rexp": ["the", "bird"]}
|
||
)
|
||
|
||
expr = (expr_a | expr_b)("rexp")
|
||
success, _ = expr.run_tests(
|
||
"""\
|
||
not the bird
|
||
the bird
|
||
"""
|
||
)
|
||
self.assertTrue(success)
|
||
|
||
result = expr.parse_string("not the bird", parse_all=True)
|
||
self.assertParseResultsEquals(
|
||
result, ["not", "the", "bird"], {"rexp": ["not", "the", "bird"]}
|
||
)
|
||
result = expr.parse_string("the bird", parse_all=True)
|
||
self.assertParseResultsEquals(
|
||
result, ["the", "bird"], {"rexp": ["the", "bird"]}
|
||
)
|
||
|
||
# test compatibility mode, no longer restoring pre-2.3.1 behavior
|
||
with ppt.reset_pyparsing_context():
|
||
pp.__compat__.collect_all_And_tokens = False
|
||
pp.enable_diag(pp.Diagnostics.warn_multiple_tokens_in_named_alternation)
|
||
expr_a = pp.Literal("not") + pp.Literal("the") + pp.Literal("bird")
|
||
expr_b = pp.Literal("the") + pp.Literal("bird")
|
||
|
||
with self.assertWarns(
|
||
UserWarning, msg="failed to warn of And within alternation"
|
||
):
|
||
expr = (expr_a ^ expr_b)("rexp")
|
||
|
||
with self.assertDoesNotWarn(
|
||
UserWarning,
|
||
msg="warned when And within alternation warning was suppressed",
|
||
):
|
||
expr = (expr_a ^ expr_b).suppress_warning(
|
||
pp.Diagnostics.warn_multiple_tokens_in_named_alternation
|
||
)("rexp")
|
||
|
||
success, _ = expr.run_tests(
|
||
"""\
|
||
not the bird
|
||
the bird
|
||
"""
|
||
)
|
||
self.assertTrue(success)
|
||
self.assertEqual(
|
||
"not the bird".split(),
|
||
list(expr.parse_string("not the bird", parse_all=True)["rexp"]),
|
||
)
|
||
self.assertEqual(
|
||
"the bird".split(),
|
||
list(expr.parse_string("the bird", parse_all=True)["rexp"]),
|
||
)
|
||
|
||
def testEmptyDictDoesNotRaiseException(self):
|
||
key = pp.Word(pp.alphas)
|
||
value = pp.Word(pp.nums)
|
||
EQ = pp.Suppress("=")
|
||
key_value_dict = pp.dict_of(key, EQ + value)
|
||
|
||
print(
|
||
key_value_dict.parse_string(
|
||
"""\
|
||
a = 10
|
||
b = 20
|
||
""",
|
||
parse_all=True,
|
||
).dump()
|
||
)
|
||
|
||
try:
|
||
print(key_value_dict.parse_string("", parse_all=True).dump())
|
||
except pp.ParseException as pe:
|
||
print(pp.ParseException.explain(pe))
|
||
else:
|
||
self.fail("failed to raise exception when matching empty string")
|
||
|
||
def testEmptyDictReturnsDictWhenCallingAsDict(self):
|
||
parser1 = pp.Dict(pp.ZeroOrMore(pp.Group(pp.Word(pp.alphas) + pp.Word(pp.alphas))))
|
||
parser2 = parser1.copy()('fubar')
|
||
|
||
p1_result = parser1.parse_string('foo bar').as_dict() # {'foo': 'bar'} ✓
|
||
p2_result = parser2.parse_string('foo bar').as_dict() # {'fubar': {'foo': 'bar'}} ✓
|
||
self.assertEqual({"foo": "bar"}, p1_result)
|
||
self.assertEqual({"fubar": {"foo": "bar"}}, p2_result)
|
||
|
||
p1_result = parser1.parse_string('').as_dict() # {} ✓
|
||
p2_result = parser2.parse_string('').as_dict() # {'fubar': []} ✗ should be {'fubar': {}}
|
||
self.assertEqual({}, p1_result)
|
||
self.assertEqual({"fubar": {}}, p2_result)
|
||
|
||
def testExplainException(self):
|
||
expr = pp.Word(pp.nums).set_name("int") + pp.Word(pp.alphas).set_name("word")
|
||
try:
|
||
expr.parse_string("123 355", parse_all=True)
|
||
except pp.ParseException as pe:
|
||
print(pe.explain(depth=0))
|
||
|
||
expr = pp.Word(pp.nums).set_name("int") - pp.Word(pp.alphas).set_name("word")
|
||
try:
|
||
expr.parse_string("123 355 (test using ErrorStop)", parse_all=True)
|
||
except pp.ParseSyntaxException as pe:
|
||
print(pe.explain())
|
||
|
||
integer = pp.Word(pp.nums).set_name("int").add_parse_action(lambda t: int(t[0]))
|
||
expr = integer + integer
|
||
|
||
def divide_args(t):
|
||
integer.parse_string("A", parse_all=True)
|
||
return t[0] / t[1]
|
||
|
||
expr.add_parse_action(divide_args)
|
||
try:
|
||
expr.parse_string("123 0", parse_all=True)
|
||
except pp.ParseException as pe:
|
||
print(pe.explain())
|
||
except Exception as exc:
|
||
print(pp.ParseBaseException.explain_exception(exc))
|
||
raise
|
||
|
||
def testExplainExceptionWithMemoizationCheck(self):
|
||
if pp.ParserElement._left_recursion_enabled or pp.ParserElement._packratEnabled:
|
||
print("test does local memoization enable/disable during test")
|
||
return
|
||
|
||
pp.ParserElement.disable_memoization()
|
||
|
||
integer = pp.Word(pp.nums).set_name("int").add_parse_action(lambda t: int(t[0]))
|
||
expr = integer + integer
|
||
|
||
def divide_args(t):
|
||
integer.parse_string("A", parse_all=True)
|
||
return t[0] / t[1]
|
||
|
||
expr.add_parse_action(divide_args)
|
||
for memo_kind, enable_memo in [
|
||
("Packrat", pp.ParserElement.enable_packrat),
|
||
("Left Recursion", pp.ParserElement.enable_left_recursion),
|
||
]:
|
||
enable_memo(force=True)
|
||
print("Explain for", memo_kind)
|
||
|
||
try:
|
||
expr.parse_string("123 0", parse_all=True)
|
||
except pp.ParseException as pe:
|
||
print(pe.explain())
|
||
except Exception as exc:
|
||
print(pp.ParseBaseException.explain_exception(exc))
|
||
raise
|
||
|
||
# make sure we leave the state compatible with everything
|
||
pp.ParserElement.disable_memoization()
|
||
|
||
def testCaselessKeywordVsKeywordCaseless(self):
|
||
frule = pp.Keyword("t", caseless=True) + pp.Keyword("yes", caseless=True)
|
||
crule = pp.CaselessKeyword("t") + pp.CaselessKeyword("yes")
|
||
|
||
flist = frule.search_string("not yes").as_list()
|
||
print(flist)
|
||
clist = crule.search_string("not yes").as_list()
|
||
print(clist)
|
||
self.assertEqual(
|
||
flist,
|
||
clist,
|
||
"CaselessKeyword not working the same as Keyword(caseless=True)",
|
||
)
|
||
|
||
def testOneOf(self):
|
||
expr = pp.one_of("a b abb")
|
||
assert expr.pattern == "abb|a|b"
|
||
|
||
expr = pp.one_of("a abb b abb")
|
||
assert expr.pattern == "abb|a|b"
|
||
|
||
expr = pp.one_of("a abb abbb b abb")
|
||
assert expr.pattern == "abbb|abb|a|b"
|
||
|
||
expr = pp.one_of("a abbb abb b abb")
|
||
assert expr.pattern == "abbb|abb|a|b"
|
||
|
||
# make sure regex-unsafe characters are properly escaped
|
||
expr = pp.one_of("a+ b* c? () +a *b ?c")
|
||
assert expr.pattern == r"a\+|b\*|c\?|\(\)|\+a|\*b|\?c"
|
||
|
||
def testOneOfKeywords(self):
|
||
literal_expr = pp.one_of("a b c")
|
||
success, _ = literal_expr[...].run_tests(
|
||
"""
|
||
# literal one_of tests
|
||
a b c
|
||
a a a
|
||
abc
|
||
"""
|
||
)
|
||
self.assertTrue(success, "failed literal one_of matching")
|
||
|
||
keyword_expr = pp.one_of("a b c", as_keyword=True)
|
||
success, _ = keyword_expr[...].run_tests(
|
||
"""
|
||
# keyword one_of tests
|
||
a b c
|
||
a a a
|
||
"""
|
||
)
|
||
self.assertTrue(success, "failed keyword one_of matching")
|
||
|
||
success, _ = keyword_expr[...].run_tests(
|
||
"""
|
||
# keyword one_of failure tests
|
||
abc
|
||
""",
|
||
failure_tests=True,
|
||
)
|
||
self.assertTrue(success, "failed keyword one_of failure tests")
|
||
|
||
def testWarnUngroupedNamedTokens(self):
|
||
"""
|
||
- warn_ungrouped_named_tokens_in_collection - flag to enable warnings when a results
|
||
name is defined on a containing expression with ungrouped subexpressions that also
|
||
have results names (default=True)
|
||
"""
|
||
with self.assertDoesNotWarn(
|
||
msg=f"raised {pp.Diagnostics.warn_ungrouped_named_tokens_in_collection} warning when not enabled"
|
||
):
|
||
COMMA = pp.Suppress(",").set_name("comma")
|
||
coord = ppc.integer("x") + COMMA + ppc.integer("y")
|
||
path = coord[...].set_results_name("path")
|
||
|
||
with ppt.reset_pyparsing_context():
|
||
pp.enable_diag(pp.Diagnostics.warn_ungrouped_named_tokens_in_collection)
|
||
|
||
COMMA = pp.Suppress(",").set_name("comma")
|
||
coord = ppc.integer("x") + COMMA + ppc.integer("y")
|
||
|
||
# this should emit a warning
|
||
with self.assertWarns(
|
||
UserWarning,
|
||
msg="failed to warn with named repetition of"
|
||
" ungrouped named expressions",
|
||
):
|
||
path = coord[...].set_results_name("path")
|
||
|
||
with self.assertDoesNotWarn(
|
||
UserWarning,
|
||
msg="warned when named repetition of"
|
||
" ungrouped named expressions warning was suppressed",
|
||
):
|
||
path = (
|
||
coord[...]
|
||
.suppress_warning(
|
||
pp.Diagnostics.warn_ungrouped_named_tokens_in_collection
|
||
)
|
||
.set_results_name("path")
|
||
)
|
||
|
||
def testDontWarnUngroupedNamedTokensIfWarningSuppressed(self):
|
||
with ppt.reset_pyparsing_context():
|
||
pp.enable_diag(pp.Diagnostics.warn_ungrouped_named_tokens_in_collection)
|
||
|
||
with self.assertDoesNotWarn(
|
||
msg=f"raised {pp.Diagnostics.warn_ungrouped_named_tokens_in_collection}"
|
||
f" warning when warn on ungrouped named tokens was suppressed (original_text_for)"
|
||
):
|
||
pp.original_text_for(pp.Word("ABC")[...])("words")
|
||
|
||
def testWarnNameSetOnEmptyForward(self):
|
||
"""
|
||
- warn_name_set_on_empty_Forward - flag to enable warnings when a Forward is defined
|
||
with a results name, but has no contents defined (default=False)
|
||
"""
|
||
|
||
with self.assertDoesNotWarn(
|
||
msg=f"raised {pp.Diagnostics.warn_name_set_on_empty_Forward} warning when not enabled"
|
||
):
|
||
base = pp.Forward()("z")
|
||
|
||
with ppt.reset_pyparsing_context():
|
||
pp.enable_diag(pp.Diagnostics.warn_name_set_on_empty_Forward)
|
||
|
||
base = pp.Forward()
|
||
|
||
with self.assertWarns(
|
||
UserWarning,
|
||
msg="failed to warn when naming an empty Forward expression",
|
||
):
|
||
base("x")
|
||
|
||
with self.assertDoesNotWarn(
|
||
UserWarning,
|
||
msg="warned when naming an empty Forward expression warning was suppressed",
|
||
):
|
||
base.suppress_warning(pp.Diagnostics.warn_name_set_on_empty_Forward)(
|
||
"x"
|
||
)
|
||
|
||
def testWarnParsingEmptyForward(self):
|
||
"""
|
||
- warn_on_parse_using_empty_Forward - flag to enable warnings when a Forward
|
||
has no contents defined (default=False)
|
||
"""
|
||
|
||
with self.assertDoesNotWarn(
|
||
msg=f"raised {pp.Diagnostics.warn_on_parse_using_empty_Forward} warning when not enabled"
|
||
):
|
||
base = pp.Forward()
|
||
try:
|
||
print(base.parse_string("x", parse_all=True))
|
||
except ParseException as pe:
|
||
pass
|
||
|
||
with ppt.reset_pyparsing_context():
|
||
pp.enable_diag(pp.Diagnostics.warn_on_parse_using_empty_Forward)
|
||
|
||
base = pp.Forward()
|
||
|
||
with self.assertWarns(
|
||
UserWarning,
|
||
msg="failed to warn when parsing using an empty Forward expression",
|
||
):
|
||
try:
|
||
print(base.parse_string("x", parse_all=True))
|
||
except ParseException as pe:
|
||
pass
|
||
|
||
with self.assertDoesNotWarn(
|
||
UserWarning,
|
||
msg="warned when parsing using an empty Forward expression warning was suppressed",
|
||
):
|
||
base.suppress_warning(pp.Diagnostics.warn_on_parse_using_empty_Forward)
|
||
try:
|
||
print(base.parse_string("x", parse_all=True))
|
||
except ParseException as pe:
|
||
pass
|
||
|
||
def testWarnIncorrectAssignmentToForward(self):
|
||
"""
|
||
- warn_on_parse_using_empty_Forward - flag to enable warnings when a Forward
|
||
has no contents defined (default=False)
|
||
"""
|
||
if PYPY_ENV:
|
||
print("warn_on_assignment_to_Forward not supported on PyPy")
|
||
return
|
||
|
||
def a_method():
|
||
base = pp.Forward()
|
||
base = pp.Word(pp.alphas)[...] | "(" + base + ")"
|
||
|
||
with self.assertDoesNotWarn(
|
||
msg=f"raised {pp.Diagnostics.warn_on_assignment_to_Forward} warning when not enabled"
|
||
):
|
||
a_method()
|
||
|
||
with ppt.reset_pyparsing_context():
|
||
pp.enable_diag(pp.Diagnostics.warn_on_assignment_to_Forward)
|
||
|
||
with self.assertWarns(
|
||
UserWarning,
|
||
msg="failed to warn when using '=' to assign expression to a Forward",
|
||
):
|
||
a_method()
|
||
|
||
def a_method():
|
||
base = pp.Forward().suppress_warning(
|
||
pp.Diagnostics.warn_on_assignment_to_Forward
|
||
)
|
||
base = pp.Word(pp.alphas)[...] | "(" + base + ")"
|
||
|
||
with self.assertDoesNotWarn(
|
||
UserWarning,
|
||
msg="warned when using '=' to assign expression to a Forward warning was suppressed",
|
||
):
|
||
a_method()
|
||
|
||
def testWarnOnMultipleStringArgsToOneOf(self):
|
||
"""
|
||
- warn_on_multiple_string_args_to_oneof - flag to enable warnings when one_of is
|
||
incorrectly called with multiple str arguments (default=True)
|
||
"""
|
||
with self.assertDoesNotWarn(
|
||
msg=f"raised {pp.Diagnostics.warn_on_multiple_string_args_to_oneof} warning when not enabled"
|
||
):
|
||
a = pp.one_of("A", "B")
|
||
|
||
with ppt.reset_pyparsing_context():
|
||
pp.enable_diag(pp.Diagnostics.warn_on_multiple_string_args_to_oneof)
|
||
|
||
with self.assertWarns(
|
||
UserWarning,
|
||
msg="failed to warn when incorrectly calling one_of(string, string)",
|
||
):
|
||
a = pp.one_of("A", "B")
|
||
|
||
def testAutonameElements(self):
|
||
with ppt.reset_pyparsing_context():
|
||
pp.enable_diag(pp.Diagnostics.enable_debug_on_named_expressions)
|
||
|
||
a = pp.Literal("a")
|
||
b = pp.Literal("b").set_name("bbb")
|
||
z = pp.Literal("z")
|
||
leading_a = a + pp.FollowedBy(z | a | b)
|
||
|
||
grammar = (z | leading_a | b)[...] + "a"
|
||
|
||
self.assertFalse(a.debug)
|
||
self.assertFalse(a.customName)
|
||
pp.autoname_elements()
|
||
self.assertTrue(a.debug)
|
||
self.assertEqual("a", a.name)
|
||
self.assertEqual("bbb", b.name)
|
||
|
||
def testDelimitedListName(self):
|
||
bool_constant = pp.Literal("True") | "true" | "False" | "false"
|
||
bool_list = pp.DelimitedList(bool_constant)
|
||
print(bool_list)
|
||
self.assertEqual(
|
||
"{'True' | 'true' | 'False' | 'false'} [, {'True' | 'true' | 'False' | 'false'}]...",
|
||
str(bool_list),
|
||
)
|
||
|
||
bool_constant.set_name("bool")
|
||
print(bool_constant)
|
||
print(bool_constant.streamline())
|
||
bool_list2 = pp.DelimitedList(bool_constant)
|
||
print(bool_constant)
|
||
print(bool_constant.streamline())
|
||
print(bool_list2)
|
||
with self.subTest():
|
||
self.assertEqual("bool [, bool]...", str(bool_list2))
|
||
|
||
with self.subTest():
|
||
street_address = pp.common.integer.set_name("integer") + pp.Word(pp.alphas)[
|
||
1, ...
|
||
].set_name("street_name")
|
||
self.assertEqual(
|
||
"{integer street_name} [, {integer street_name}]...",
|
||
str(pp.DelimitedList(street_address)),
|
||
)
|
||
|
||
with self.subTest():
|
||
operand = pp.Char(pp.alphas).set_name("var")
|
||
math = pp.infix_notation(
|
||
operand,
|
||
[
|
||
(pp.one_of("+ -"), 2, pp.OpAssoc.LEFT),
|
||
],
|
||
)
|
||
self.assertEqual(
|
||
"var_expression [, var_expression]...",
|
||
str(pp.DelimitedList(math)),
|
||
)
|
||
|
||
def testDelimitedListOfStrLiterals(self):
|
||
expr = pp.DelimitedList("ABC")
|
||
print(str(expr))
|
||
source = "ABC, ABC,ABC"
|
||
self.assertParseAndCheckList(
|
||
expr, source, [s.strip() for s in source.split(",")]
|
||
)
|
||
|
||
def testDelimitedListMinMax(self):
|
||
source = "ABC, ABC,ABC"
|
||
with self.assertRaises(ValueError, msg="min must be greater than 0"):
|
||
pp.DelimitedList("ABC", min=0)
|
||
with self.assertRaises(
|
||
ValueError, msg="max must be greater than, or equal to min"
|
||
):
|
||
pp.DelimitedList("ABC", min=1, max=0)
|
||
with self.assertRaises(pp.ParseException):
|
||
pp.DelimitedList("ABC", min=4).parse_string(source)
|
||
|
||
source_expr_pairs = [
|
||
("ABC, ABC", pp.DelimitedList("ABC", max=2)),
|
||
(source, pp.DelimitedList("ABC", min=2, max=4)),
|
||
]
|
||
for source, expr in source_expr_pairs:
|
||
print(str(expr))
|
||
self.assertParseAndCheckList(
|
||
expr, source, [s.strip() for s in source.split(",")]
|
||
)
|
||
|
||
def testDelimitedListParseActions1(self):
|
||
# from issue #408
|
||
keyword = pp.Keyword("foobar")
|
||
untyped_identifier = ~keyword + pp.Word(pp.alphas)
|
||
dotted_vars = pp.DelimitedList(untyped_identifier, delim=".")
|
||
lvalue = pp.Opt(dotted_vars)
|
||
|
||
# uncomment this line to see the problem
|
||
stmt = pp.DelimitedList(pp.Opt(dotted_vars))
|
||
# stmt = DelimitedList(dotted_vars)
|
||
# stmt = pp.Opt(dotted_vars)
|
||
|
||
def parse_identifier(toks):
|
||
print("YAY!", toks)
|
||
|
||
untyped_identifier.set_parse_action(parse_identifier)
|
||
|
||
save_stdout = StringIO()
|
||
with contextlib.redirect_stdout(save_stdout):
|
||
dotted_vars.parse_string("B.C")
|
||
|
||
self.assertEqual(
|
||
dedent(
|
||
"""\
|
||
YAY! ['B']
|
||
YAY! ['C']
|
||
"""
|
||
),
|
||
save_stdout.getvalue(),
|
||
)
|
||
|
||
def testDelimitedListParseActions2(self):
|
||
# from issue #408
|
||
keyword = pp.Keyword("foobar")
|
||
untyped_identifier = ~keyword + pp.Word(pp.alphas)
|
||
dotted_vars = pp.DelimitedList(untyped_identifier, delim=".")
|
||
lvalue = pp.Opt(dotted_vars)
|
||
|
||
# uncomment this line to see the problem
|
||
# stmt = DelimitedList(Opt(dotted_vars))
|
||
stmt = pp.DelimitedList(dotted_vars)
|
||
# stmt = pp.Opt(dotted_vars)
|
||
|
||
def parse_identifier(toks):
|
||
print("YAY!", toks)
|
||
|
||
untyped_identifier.set_parse_action(parse_identifier)
|
||
|
||
save_stdout = StringIO()
|
||
with contextlib.redirect_stdout(save_stdout):
|
||
dotted_vars.parse_string("B.C")
|
||
|
||
self.assertEqual(
|
||
dedent(
|
||
"""\
|
||
YAY! ['B']
|
||
YAY! ['C']
|
||
"""
|
||
),
|
||
save_stdout.getvalue(),
|
||
)
|
||
|
||
def testDelimitedListParseActions3(self):
|
||
# from issue #408
|
||
keyword = pp.Keyword("foobar")
|
||
untyped_identifier = ~keyword + pp.Word(pp.alphas)
|
||
dotted_vars = pp.DelimitedList(untyped_identifier, delim=".")
|
||
lvalue = pp.Opt(dotted_vars)
|
||
|
||
# uncomment this line to see the problem
|
||
# stmt = DelimitedList(Opt(dotted_vars))
|
||
# stmt = DelimitedList(dotted_vars)
|
||
stmt = pp.Opt(dotted_vars)
|
||
|
||
def parse_identifier(toks):
|
||
print("YAY!", toks)
|
||
|
||
untyped_identifier.set_parse_action(parse_identifier)
|
||
|
||
save_stdout = StringIO()
|
||
with contextlib.redirect_stdout(save_stdout):
|
||
dotted_vars.parse_string("B.C")
|
||
|
||
self.assertEqual(
|
||
dedent(
|
||
"""\
|
||
YAY! ['B']
|
||
YAY! ['C']
|
||
"""
|
||
),
|
||
save_stdout.getvalue(),
|
||
)
|
||
|
||
def testTagElements(self):
|
||
end_punc = (
|
||
("." + pp.Tag("mood", "normal"))
|
||
| ("!" + pp.Tag("mood", "excited"))
|
||
| ("?" + pp.Tag("mood", "curious"))
|
||
)
|
||
greeting = "Hello" + pp.Word(pp.alphas) + end_punc[1, ...]
|
||
|
||
for ending, expected_mood in [
|
||
(".", "normal"),
|
||
("!", "excited"),
|
||
("?", "curious"),
|
||
("!!", "excited"),
|
||
("!?", "curious"),
|
||
]:
|
||
self.assertParseAndCheckDict(
|
||
greeting, f"Hello World{ending}", {"mood": expected_mood}
|
||
)
|
||
|
||
def testEnableDebugOnNamedExpressions(self):
|
||
"""
|
||
- enable_debug_on_named_expressions - flag to auto-enable debug on all subsequent
|
||
calls to ParserElement.set_name() (default=False)
|
||
"""
|
||
with ppt.reset_pyparsing_context():
|
||
test_stdout = StringIO()
|
||
|
||
with resetting(sys, "stdout", "stderr"):
|
||
sys.stdout = test_stdout
|
||
sys.stderr = test_stdout
|
||
|
||
pp.enable_diag(pp.Diagnostics.enable_debug_on_named_expressions)
|
||
integer = pp.Word(pp.nums).set_name("integer")
|
||
|
||
integer[...].parse_string("1 2 3", parse_all=True)
|
||
|
||
expected_debug_output = dedent(
|
||
"""\
|
||
Match integer at loc 0(1,1)
|
||
1 2 3
|
||
^
|
||
Matched integer -> ['1']
|
||
Match integer at loc 2(1,3)
|
||
1 2 3
|
||
^
|
||
Matched integer -> ['2']
|
||
Match integer at loc 4(1,5)
|
||
1 2 3
|
||
^
|
||
Matched integer -> ['3']
|
||
Match integer at loc 5(1,6)
|
||
1 2 3
|
||
^
|
||
Match integer failed, ParseException raised: Expected integer, found end of text (at char 5), (line:1, col:6)
|
||
"""
|
||
)
|
||
output = test_stdout.getvalue()
|
||
print(output)
|
||
self.assertEqual(
|
||
expected_debug_output,
|
||
output,
|
||
"failed to auto-enable debug on named expressions "
|
||
"using enable_debug_on_named_expressions",
|
||
)
|
||
|
||
def testEnableDebugOnExpressionWithParseAction(self):
|
||
test_stdout = StringIO()
|
||
with resetting(sys, "stdout", "stderr"):
|
||
sys.stdout = test_stdout
|
||
sys.stderr = test_stdout
|
||
|
||
parser = (ppc.integer().set_debug() | pp.Word(pp.alphanums).set_debug())[
|
||
...
|
||
]
|
||
parser.set_debug()
|
||
parser.parse_string("123 A100", parse_all=True)
|
||
|
||
# now turn off debug - should only get output for components, not overall parser
|
||
print()
|
||
parser.set_debug(False)
|
||
parser.parse_string("123 A100", parse_all=True)
|
||
|
||
expected_debug_output = dedent(
|
||
"""\
|
||
Match [{integer | W:(0-9A-Za-z)}]... at loc 0(1,1)
|
||
123 A100
|
||
^
|
||
Match integer at loc 0(1,1)
|
||
123 A100
|
||
^
|
||
Matched integer -> [123]
|
||
Match integer at loc 4(1,5)
|
||
123 A100
|
||
^
|
||
Match integer failed, ParseException raised: Expected integer, found 'A100' (at char 4), (line:1, col:5)
|
||
Match W:(0-9A-Za-z) at loc 4(1,5)
|
||
123 A100
|
||
^
|
||
Matched W:(0-9A-Za-z) -> ['A100']
|
||
Match integer at loc 8(1,9)
|
||
123 A100
|
||
^
|
||
Match integer failed, ParseException raised: Expected integer, found end of text (at char 8), (line:1, col:9)
|
||
Match W:(0-9A-Za-z) at loc 8(1,9)
|
||
123 A100
|
||
^
|
||
Match W:(0-9A-Za-z) failed, ParseException raised: Expected W:(0-9A-Za-z), found end of text (at char 8), (line:1, col:9)
|
||
Matched [{integer | W:(0-9A-Za-z)}]... -> [123, 'A100']
|
||
|
||
Match integer at loc 0(1,1)
|
||
123 A100
|
||
^
|
||
Matched integer -> [123]
|
||
Match integer at loc 4(1,5)
|
||
123 A100
|
||
^
|
||
Match integer failed, ParseException raised: Expected integer, found 'A100' (at char 4), (line:1, col:5)
|
||
Match W:(0-9A-Za-z) at loc 4(1,5)
|
||
123 A100
|
||
^
|
||
Matched W:(0-9A-Za-z) -> ['A100']
|
||
Match integer at loc 8(1,9)
|
||
123 A100
|
||
^
|
||
Match integer failed, ParseException raised: Expected integer, found end of text (at char 8), (line:1, col:9)
|
||
Match W:(0-9A-Za-z) at loc 8(1,9)
|
||
123 A100
|
||
^
|
||
Match W:(0-9A-Za-z) failed, ParseException raised: Expected W:(0-9A-Za-z), found end of text (at char 8), (line:1, col:9)
|
||
"""
|
||
)
|
||
output = test_stdout.getvalue()
|
||
print(output)
|
||
self.assertEqual(
|
||
expected_debug_output,
|
||
output,
|
||
"invalid debug output when using parse action",
|
||
)
|
||
|
||
def testSetDebugWithControlCharInLine(self):
|
||
# issue #496 - a '\r' embedded in the parsed line used to be printed
|
||
# verbatim, sending the terminal cursor back to column 0 and corrupting
|
||
# the debug output; control characters should be shown escaped instead
|
||
with ppt.reset_pyparsing_context():
|
||
test_stdout = StringIO()
|
||
|
||
with resetting(sys, "stdout", "stderr"):
|
||
sys.stdout = test_stdout
|
||
sys.stderr = test_stdout
|
||
|
||
word = pp.Word(pp.alphas).set_name("word").set_debug()
|
||
word[...].parse_string("abc\rdef", parse_all=True)
|
||
|
||
expected_debug_output = dedent(
|
||
"""\
|
||
Match word at loc 0(1,1)
|
||
abc\\rdef
|
||
^
|
||
Matched word -> ['abc']
|
||
Match word at loc 4(1,5)
|
||
abc\\rdef
|
||
^
|
||
Matched word -> ['def']
|
||
Match word at loc 7(1,8)
|
||
abc\\rdef
|
||
^
|
||
Match word failed, ParseException raised: Expected word, found end of text (at char 7), (line:1, col:8)
|
||
"""
|
||
)
|
||
self.assertEqual(
|
||
expected_debug_output,
|
||
test_stdout.getvalue(),
|
||
"control characters in debug output were not escaped",
|
||
)
|
||
|
||
def testEnableDebugWithCachedExpressionsMarkedWithAsterisk(self):
|
||
a = pp.Literal("a").set_name("A").set_debug()
|
||
b = pp.Literal("b").set_name("B").set_debug()
|
||
z = pp.Literal("z").set_name("Z").set_debug()
|
||
leading_a = a + pp.FollowedBy(z | a | b)
|
||
leading_a.set_name("leading_a").set_debug()
|
||
|
||
grammar = (z | leading_a | b)[...] + "a"
|
||
|
||
# parse test string and capture debug output
|
||
test_stdout = StringIO()
|
||
with resetting(sys, "stdout", "stderr"):
|
||
sys.stdout = test_stdout
|
||
sys.stderr = test_stdout
|
||
grammar.parse_string("aba", parse_all=True)
|
||
|
||
expected_debug_output = dedent(
|
||
"""\
|
||
Match Z at loc 0(1,1)
|
||
aba
|
||
^
|
||
Match Z failed, ParseException raised: Expected Z, found 'aba' (at char 0), (line:1, col:1)
|
||
Match leading_a at loc 0(1,1)
|
||
aba
|
||
^
|
||
Match A at loc 0(1,1)
|
||
aba
|
||
^
|
||
Matched A -> ['a']
|
||
Match Z at loc 1(1,2)
|
||
aba
|
||
^
|
||
Match Z failed, ParseException raised: Expected Z, found 'ba' (at char 1), (line:1, col:2)
|
||
Match A at loc 1(1,2)
|
||
aba
|
||
^
|
||
Match A failed, ParseException raised: Expected A, found 'ba' (at char 1), (line:1, col:2)
|
||
Match B at loc 1(1,2)
|
||
aba
|
||
^
|
||
Matched B -> ['b']
|
||
Matched leading_a -> ['a']
|
||
*Match Z at loc 1(1,2)
|
||
aba
|
||
^
|
||
*Match Z failed, ParseException raised: Expected Z, found 'ba' (at char 1), (line:1, col:2)
|
||
Match leading_a at loc 1(1,2)
|
||
aba
|
||
^
|
||
Match A at loc 1(1,2)
|
||
aba
|
||
^
|
||
Match A failed, ParseException raised: Expected A, found 'ba' (at char 1), (line:1, col:2)
|
||
Match leading_a failed, ParseException raised: Expected A, found 'ba' (at char 1), (line:1, col:2)
|
||
*Match B at loc 1(1,2)
|
||
aba
|
||
^
|
||
*Matched B -> ['b']
|
||
Match Z at loc 2(1,3)
|
||
aba
|
||
^
|
||
Match Z failed, ParseException raised: Expected Z, found 'a' (at char 2), (line:1, col:3)
|
||
Match leading_a at loc 2(1,3)
|
||
aba
|
||
^
|
||
Match A at loc 2(1,3)
|
||
aba
|
||
^
|
||
Matched A -> ['a']
|
||
Match Z at loc 3(1,4)
|
||
aba
|
||
^
|
||
Match Z failed, ParseException raised: Expected Z, found end of text (at char 3), (line:1, col:4)
|
||
Match A at loc 3(1,4)
|
||
aba
|
||
^
|
||
Match A failed, ParseException raised: Expected A, found end of text (at char 3), (line:1, col:4)
|
||
Match B at loc 3(1,4)
|
||
aba
|
||
^
|
||
Match B failed, ParseException raised: Expected B, found end of text (at char 3), (line:1, col:4)
|
||
Match leading_a failed, ParseException raised: Expected {Z | A | B}, found end of text (at char 3), (line:1, col:4)
|
||
Match B at loc 2(1,3)
|
||
aba
|
||
^
|
||
Match B failed, ParseException raised: Expected B, found 'a' (at char 2), (line:1, col:3)
|
||
"""
|
||
)
|
||
if pp.ParserElement._packratEnabled:
|
||
packrat_status = "enabled"
|
||
else:
|
||
# remove '*' cache markers from expected output
|
||
expected_debug_output = expected_debug_output.replace("*", "")
|
||
packrat_status = "disabled"
|
||
print("Packrat status:", packrat_status)
|
||
|
||
output = test_stdout.getvalue()
|
||
print(output)
|
||
self.assertEqual(
|
||
expected_debug_output,
|
||
output,
|
||
(
|
||
f"invalid debug output showing cached results marked with '*',"
|
||
f" and packrat parsing {packrat_status}"
|
||
),
|
||
)
|
||
|
||
def testSetDebugRecursively(self):
|
||
expr = pp.Word(pp.alphas)
|
||
contained = expr + pp.Empty().set_name("innermost")
|
||
depth = 4
|
||
for _ in range(depth):
|
||
contained = pp.Group(contained + pp.Empty())
|
||
contained.set_debug(recurse=True)
|
||
self.assertTrue(expr.debug)
|
||
# contained.parse_string("ABC")
|
||
test_stdout = StringIO()
|
||
with resetting(sys, "stdout", "stderr"):
|
||
sys.stdout = test_stdout
|
||
sys.stderr = test_stdout
|
||
contained.parse_string("aba", parse_all=True)
|
||
|
||
output = test_stdout.getvalue()
|
||
print(output)
|
||
self.assertEqual(depth, output.count("Matched Empty -> []"))
|
||
self.assertEqual(1, output.count("Matched innermost -> []"))
|
||
|
||
def testSetDebugRecursivelyWithForward(self):
|
||
expr = pp.Word(pp.alphas).set_name("innermost")
|
||
contained = pp.infix_notation(
|
||
expr,
|
||
[
|
||
("NOT", 1, pp.OpAssoc.RIGHT),
|
||
("AND", 2, pp.OpAssoc.LEFT),
|
||
("OR", 2, pp.OpAssoc.LEFT),
|
||
],
|
||
)
|
||
|
||
contained.set_debug(recurse=True)
|
||
self.assertTrue(expr.debug)
|
||
|
||
# contained.parse_string("ABC")
|
||
test_stdout = StringIO()
|
||
with resetting(sys, "stdout", "stderr"):
|
||
sys.stdout = test_stdout
|
||
sys.stderr = test_stdout
|
||
contained.parse_string("aba", parse_all=True)
|
||
|
||
output = test_stdout.getvalue()
|
||
print(output)
|
||
# count of matches varies with packrat state, can't match exact count, but at least test if contains
|
||
# self.assertEqual(4, output.count("Matched innermost -> ['aba']"))
|
||
self.assertTrue("Matched innermost -> ['aba']" in output)
|
||
|
||
def testUndesirableButCommonPractices(self):
|
||
# While these are valid constructs, and they are not encouraged
|
||
# there is apparently a lot of code out there using these
|
||
# coding styles.
|
||
#
|
||
# Even though they are not encouraged, we shouldn't break them.
|
||
|
||
# Create an And using a list of expressions instead of using '+' operator
|
||
expr = pp.And([pp.Word("abc"), pp.Word("123")])
|
||
success, _ = expr.run_tests(
|
||
"""
|
||
aaa 333
|
||
b 1
|
||
ababab 32123
|
||
"""
|
||
)
|
||
self.assertTrue(success)
|
||
|
||
success, _ = expr.run_tests(
|
||
"""\
|
||
aad 111
|
||
""",
|
||
failure_tests=True,
|
||
)
|
||
self.assertTrue(success)
|
||
|
||
# Passing a single expression to a ParseExpression, when it really wants a sequence
|
||
expr = pp.Or(pp.Or(ppc.integer))
|
||
success, _ = expr.run_tests(
|
||
"""\
|
||
123
|
||
456
|
||
"""
|
||
)
|
||
self.assertTrue(success)
|
||
|
||
success, _ = expr.run_tests(
|
||
"""\
|
||
abc
|
||
""",
|
||
failure_tests=True,
|
||
)
|
||
self.assertTrue(success)
|
||
|
||
def testEnableWarnDiags(self):
|
||
import pprint
|
||
|
||
def filtered_vars(var_dict):
|
||
dunders = [nm for nm in var_dict if nm.startswith("__")]
|
||
return {
|
||
k: v
|
||
for k, v in var_dict.items()
|
||
if isinstance(v, bool) and k not in dunders
|
||
}
|
||
|
||
pprint.pprint(filtered_vars(vars(pp.__diag__)), width=30)
|
||
|
||
warn_names = pp.__diag__._warning_names
|
||
other_names = pp.__diag__._debug_names
|
||
|
||
# make sure they are off by default
|
||
for diag_name in warn_names:
|
||
self.assertFalse(
|
||
getattr(pp.__diag__, diag_name),
|
||
f"__diag__.{diag_name} not set to True",
|
||
)
|
||
|
||
with ppt.reset_pyparsing_context():
|
||
# enable all warn_* diag_names
|
||
pp.enable_all_warnings()
|
||
pprint.pprint(filtered_vars(vars(pp.__diag__)), width=30)
|
||
|
||
# make sure they are on after being enabled
|
||
for diag_name in warn_names:
|
||
self.assertTrue(
|
||
getattr(pp.__diag__, diag_name),
|
||
f"__diag__.{diag_name} not set to True",
|
||
)
|
||
|
||
# non-warn diag_names must be enabled individually
|
||
for diag_name in other_names:
|
||
self.assertFalse(
|
||
getattr(pp.__diag__, diag_name),
|
||
f"__diag__.{diag_name} not set to True",
|
||
)
|
||
|
||
# make sure they are off after AutoReset
|
||
for diag_name in warn_names:
|
||
self.assertFalse(
|
||
getattr(pp.__diag__, diag_name),
|
||
f"__diag__.{diag_name} not set to True",
|
||
)
|
||
|
||
def testWordInternalReRangeWithConsecutiveChars(self):
|
||
self.assertParseAndCheckList(
|
||
pp.Word("ABCDEMNXYZ"),
|
||
"ABCDEMNXYZABCDEMNXYZABCDEMNXYZ",
|
||
["ABCDEMNXYZABCDEMNXYZABCDEMNXYZ"],
|
||
)
|
||
|
||
def testWordInternalReRangesKnownSet(self):
|
||
tests = [
|
||
("ABCDEMNXYZ", "[A-EMNX-Z]+"),
|
||
(pp.printables, "[!-~]+"),
|
||
(pp.alphas, "[A-Za-z]+"),
|
||
(pp.alphanums, "[0-9A-Za-z]+"),
|
||
(pp.pyparsing_unicode.Latin1.printables, "[!-~¡-ÿ]+"),
|
||
(pp.pyparsing_unicode.Latin1.alphas, "[A-Za-zªµºÀ-ÖØ-öø-ÿ]+"),
|
||
(pp.pyparsing_unicode.Latin1.alphanums, "[0-9A-Za-zª²³µ¹ºÀ-ÖØ-öø-ÿ]+"),
|
||
(pp.alphas8bit, "[À-ÖØ-öø-ÿ]+"),
|
||
]
|
||
failed = []
|
||
for word_string, expected_re in tests:
|
||
try:
|
||
msg = f"failed to generate correct internal re for {word_string!r}"
|
||
resultant_re = pp.Word(word_string).reString
|
||
self.assertEqual(
|
||
expected_re,
|
||
resultant_re,
|
||
msg + f"; expected {expected_re!r} got {resultant_re!r}",
|
||
)
|
||
except AssertionError:
|
||
failed.append(msg)
|
||
|
||
if failed:
|
||
print("Errors:\n{}".format("\n".join(failed)))
|
||
self.fail("failed to generate correct internal re's")
|
||
|
||
def testWordInternalReRanges(self):
|
||
import random
|
||
|
||
esc_chars = r"\^-]["
|
||
esc_chars2 = r"*+.?"
|
||
|
||
def esc_re_set_char(c):
|
||
return "\\" + c if c in esc_chars else c
|
||
|
||
def esc_re_set2_char(c):
|
||
return "\\" + c if c in esc_chars + esc_chars2 else c
|
||
|
||
for esc_char in esc_chars + esc_chars2:
|
||
# test escape char as first character in range
|
||
next_char = chr(ord(esc_char) + 1)
|
||
prev_char = chr(ord(esc_char) - 1)
|
||
esc_word = pp.Word(esc_char + next_char)
|
||
expected = rf"[{esc_re_set_char(esc_char)}{esc_re_set_char(next_char)}]+"
|
||
print(
|
||
f"Testing escape char: {esc_char} -> {esc_word} re: '{esc_word.reString}')"
|
||
)
|
||
self.assertEqual(
|
||
expected, esc_word.reString, "failed to generate correct internal re"
|
||
)
|
||
test_string = "".join(
|
||
random.choice([esc_char, next_char]) for __ in range(16)
|
||
)
|
||
print(
|
||
f"Match '{test_string}' -> {test_string == esc_word.parse_string(test_string, parse_all=True)[0]}"
|
||
)
|
||
self.assertEqual(
|
||
test_string,
|
||
esc_word.parse_string(test_string, parse_all=True)[0],
|
||
"Word using escaped range char failed to parse",
|
||
)
|
||
|
||
# test escape char as last character in range
|
||
esc_word = pp.Word(prev_char + esc_char)
|
||
expected = rf"[{esc_re_set_char(prev_char)}{esc_re_set_char(esc_char)}]+"
|
||
print(
|
||
f"Testing escape char: {esc_char} -> {esc_word} re: '{esc_word.reString}')"
|
||
)
|
||
self.assertEqual(
|
||
expected, esc_word.reString, "failed to generate correct internal re"
|
||
)
|
||
test_string = "".join(
|
||
random.choice([esc_char, prev_char]) for __ in range(16)
|
||
)
|
||
print(
|
||
f"Match '{test_string}' -> {test_string == esc_word.parse_string(test_string, parse_all=True)[0]}"
|
||
)
|
||
self.assertEqual(
|
||
test_string,
|
||
esc_word.parse_string(test_string, parse_all=True)[0],
|
||
"Word using escaped range char failed to parse",
|
||
)
|
||
|
||
# test escape char as first character in range
|
||
next_char = chr(ord(esc_char) + 1)
|
||
prev_char = chr(ord(esc_char) - 1)
|
||
esc_word = pp.Word(esc_char + next_char)
|
||
expected = rf"[{esc_re_set_char(esc_char)}{esc_re_set_char(next_char)}]+"
|
||
print(
|
||
f"Testing escape char: {esc_char} -> {esc_word} re: '{esc_word.reString}')"
|
||
)
|
||
self.assertEqual(
|
||
expected, esc_word.reString, "failed to generate correct internal re"
|
||
)
|
||
test_string = "".join(
|
||
random.choice([esc_char, next_char]) for __ in range(16)
|
||
)
|
||
print(
|
||
f"Match '{test_string}' -> {test_string == esc_word.parse_string(test_string, parse_all=True)[0]}"
|
||
)
|
||
self.assertEqual(
|
||
test_string,
|
||
esc_word.parse_string(test_string, parse_all=True)[0],
|
||
"Word using escaped range char failed to parse",
|
||
)
|
||
|
||
# test escape char as only character in range
|
||
esc_word = pp.Word(esc_char, pp.alphas.upper())
|
||
expected = rf"{esc_re_set2_char(esc_char)}[A-Z]*"
|
||
print(
|
||
f"Testing escape char: {esc_char} -> {esc_word} re: '{esc_word.reString}')"
|
||
)
|
||
self.assertEqual(
|
||
expected, esc_word.reString, "failed to generate correct internal re"
|
||
)
|
||
test_string = esc_char + "".join(
|
||
random.choice(pp.alphas.upper()) for __ in range(16)
|
||
)
|
||
print(
|
||
f"Match '{test_string}' -> {test_string == esc_word.parse_string(test_string, parse_all=True)[0]}"
|
||
)
|
||
self.assertEqual(
|
||
test_string,
|
||
esc_word.parse_string(test_string, parse_all=True)[0],
|
||
"Word using escaped range char failed to parse",
|
||
)
|
||
|
||
# test escape char as only character
|
||
esc_word = pp.Word(esc_char, pp.alphas.upper())
|
||
expected = rf"{re.escape(esc_char)}[A-Z]*"
|
||
print(
|
||
f"Testing escape char: {esc_char} -> {esc_word} re: '{esc_word.reString}')"
|
||
)
|
||
self.assertEqual(
|
||
expected, esc_word.reString, "failed to generate correct internal re"
|
||
)
|
||
test_string = esc_char + "".join(
|
||
random.choice(pp.alphas.upper()) for __ in range(16)
|
||
)
|
||
print(
|
||
f"Match '{test_string}' -> {test_string == esc_word.parse_string(test_string, parse_all=True)[0]}"
|
||
)
|
||
self.assertEqual(
|
||
test_string,
|
||
esc_word.parse_string(test_string, parse_all=True)[0],
|
||
"Word using escaped range char failed to parse",
|
||
)
|
||
print()
|
||
|
||
def testWordWithIdentChars(self):
|
||
ppu = pp.pyparsing_unicode
|
||
|
||
latin_identifier = pp.Word(pp.identchars, pp.identbodychars)("latin*")
|
||
japanese_identifier = ppu.Japanese.identifier("japanese*")
|
||
cjk_identifier = ppu.CJK.identifier("cjk*")
|
||
greek_identifier = ppu.Greek.identifier("greek*")
|
||
cyrillic_identifier = ppu.Cyrillic.identifier("cyrillic*")
|
||
thai_identifier = ppu.Thai.identifier("thai*")
|
||
idents = (
|
||
latin_identifier
|
||
| japanese_identifier
|
||
| cjk_identifier # must follow japanese_identifier, since CJK is superset
|
||
| thai_identifier
|
||
| greek_identifier
|
||
| cyrillic_identifier
|
||
)
|
||
|
||
result = idents[...].parse_string(
|
||
"abc_100 кириллицаx_10 日本語f_300 ไทยg_600 def_200 漢字y_300 한국어_中文c_400 Ελληνικάb_500",
|
||
parse_all=True,
|
||
)
|
||
self.assertParseResultsEquals(
|
||
result,
|
||
[
|
||
"abc_100",
|
||
"кириллицаx_10",
|
||
"日本語f_300",
|
||
"ไทยg_600",
|
||
"def_200",
|
||
"漢字y_300",
|
||
"한국어_中文c_400",
|
||
"Ελληνικάb_500",
|
||
],
|
||
{
|
||
"cjk": ["한국어_中文c_400"],
|
||
"cyrillic": ["кириллицаx_10"],
|
||
"greek": ["Ελληνικάb_500"],
|
||
"japanese": ["日本語f_300", "漢字y_300"],
|
||
"latin": ["abc_100", "def_200"],
|
||
"thai": ["ไทยg_600"],
|
||
},
|
||
)
|
||
|
||
def testChainedTernaryOperator(self):
|
||
# fmt: off
|
||
TERNARY_INFIX = pp.infix_notation(
|
||
ppc.integer,
|
||
[
|
||
(("?", ":"), 3, pp.OpAssoc.LEFT),
|
||
]
|
||
)
|
||
self.assertParseAndCheckList(
|
||
TERNARY_INFIX, "1?1:0?1:0", [[1, "?", 1, ":", 0, "?", 1, ":", 0]]
|
||
)
|
||
|
||
TERNARY_INFIX = pp.infix_notation(
|
||
ppc.integer,
|
||
[
|
||
(("?", ":"), 3, pp.OpAssoc.RIGHT),
|
||
]
|
||
)
|
||
self.assertParseAndCheckList(
|
||
TERNARY_INFIX, "1?1:0?1:0", [[1, "?", 1, ":", [0, "?", 1, ":", 0]]]
|
||
)
|
||
# fmt: on
|
||
|
||
def testOneOfWithDuplicateSymbols(self):
|
||
# test making one_of with duplicate symbols
|
||
print("verify one_of handles duplicate symbols")
|
||
try:
|
||
test1 = pp.one_of("a b c d a")
|
||
except RuntimeError:
|
||
self.fail(
|
||
"still have infinite loop in one_of with duplicate symbols (string input)"
|
||
)
|
||
|
||
print("verify one_of handles duplicate symbols")
|
||
try:
|
||
test1 = pp.one_of("a a a b c d a")
|
||
except RuntimeError:
|
||
self.fail(
|
||
"still have infinite loop in one_of with duplicate symbols (string input)"
|
||
)
|
||
|
||
assert test1.pattern == "[abcd]"
|
||
|
||
print("verify one_of handles generator input")
|
||
try:
|
||
test1 = pp.one_of(c for c in "a b c d a d d d" if not c.isspace())
|
||
except RuntimeError:
|
||
self.fail(
|
||
"still have infinite loop in one_of with duplicate symbols (generator input)"
|
||
)
|
||
|
||
assert test1.pattern == "[abcd]"
|
||
|
||
print("verify one_of handles list input")
|
||
try:
|
||
test1 = pp.one_of("a b c d a".split())
|
||
except RuntimeError:
|
||
self.fail(
|
||
"still have infinite loop in one_of with duplicate symbols (list input)"
|
||
)
|
||
|
||
assert test1.pattern == "[abcd]"
|
||
|
||
print("verify one_of handles set input")
|
||
try:
|
||
test1 = pp.one_of(set("a b c d a".split()))
|
||
except RuntimeError:
|
||
self.fail(
|
||
"still have infinite loop in one_of with duplicate symbols (set input)"
|
||
)
|
||
|
||
# set will generate scrambled letters, get pattern but resort to test
|
||
pattern_letters = test1.pattern[1:-1]
|
||
assert sorted(pattern_letters) == sorted("abcd")
|
||
|
||
def testOneOfWithEmptyList(self):
|
||
"""test one_of helper function with an empty list as input"""
|
||
|
||
tst = []
|
||
result = pp.one_of(tst)
|
||
|
||
expected = True
|
||
found = isinstance(result, pp.NoMatch)
|
||
self.assertEqual(expected, found)
|
||
|
||
def testOneOfWithUnexpectedInput(self):
|
||
"""test one_of with an input that isn't a string or iterable"""
|
||
|
||
with self.assertRaises(
|
||
TypeError, msg="failed to warn use of integer for one_of"
|
||
):
|
||
expr = pp.one_of(6)
|
||
|
||
def testMatchFirstIteratesOverAllChoices(self):
|
||
# test MatchFirst bugfix
|
||
print("verify MatchFirst iterates properly")
|
||
results = pp.quoted_string.parse_string(
|
||
"'this is a single quoted string'", parse_all=True
|
||
)
|
||
self.assertTrue(
|
||
len(results) > 0, "MatchFirst error - not iterating over all choices"
|
||
)
|
||
|
||
def testStreamlineOfExpressionsAfterSetName(self):
|
||
bool_constant = pp.Literal("True") | "true" | "False" | "false"
|
||
self.assertEqual(
|
||
"{'True' | 'true' | 'False' | 'false'}", str(bool_constant.streamline())
|
||
)
|
||
bool_constant.set_name("bool")
|
||
self.assertEqual("bool", str(bool_constant.streamline()))
|
||
|
||
def testStreamlineOfSubexpressions(self):
|
||
# verify streamline of subexpressions
|
||
print("verify proper streamline logic")
|
||
compound = pp.Literal("A") + "B" + "C" + "D"
|
||
self.assertEqual(2, len(compound.exprs), "bad test setup")
|
||
print(compound)
|
||
compound.streamline()
|
||
print(compound)
|
||
self.assertEqual(4, len(compound.exprs), "streamline not working")
|
||
|
||
def testOptionalWithResultsNameAndNoMatch(self):
|
||
# test for Optional with results name and no match
|
||
print("verify Optional's do not cause match failure if have results name")
|
||
testGrammar = pp.Literal("A") + pp.Optional("B")("gotB") + pp.Literal("C")
|
||
try:
|
||
testGrammar.parse_string("ABC", parse_all=True)
|
||
testGrammar.parse_string("AC", parse_all=True)
|
||
except pp.ParseException as pe:
|
||
print(pe.pstr, "->", pe)
|
||
self.fail(f"error in Optional matching of string {pe.pstr}")
|
||
|
||
def testReturnOfFurthestException(self):
|
||
# test return of furthest exception
|
||
testGrammar = (
|
||
pp.Literal("A") | (pp.Literal("B") + pp.Literal("C")) | pp.Literal("E")
|
||
)
|
||
try:
|
||
testGrammar.parse_string("BC", parse_all=True)
|
||
testGrammar.parse_string("BD", parse_all=True)
|
||
except pp.ParseException as pe:
|
||
print(pe.pstr, "->", pe)
|
||
self.assertEqual("BD", pe.pstr, "wrong test string failed to parse")
|
||
self.assertEqual(
|
||
1, pe.loc, "error in Optional matching, pe.loc=" + str(pe.loc)
|
||
)
|
||
self.assertTrue(
|
||
"found 'D'" in str(pe), "wrong alternative raised exception"
|
||
)
|
||
|
||
def testValidateCorrectlyDetectsInvalidLeftRecursion(self):
|
||
# test validate
|
||
print("verify behavior of validate()")
|
||
if IRON_PYTHON_ENV:
|
||
print("disable this test under IronPython")
|
||
return
|
||
|
||
def testValidation(grmr, gnam, isValid):
|
||
try:
|
||
grmr.streamline()
|
||
with self.assertWarns(
|
||
PyparsingDeprecationWarning,
|
||
msg="failed to warn validate() is deprecated",
|
||
):
|
||
grmr.validate()
|
||
self.assertTrue(isValid, "validate() accepted invalid grammar " + gnam)
|
||
except pp.RecursiveGrammarException as rge:
|
||
print(grmr)
|
||
print(rge)
|
||
self.assertFalse(isValid, "validate() rejected valid grammar " + gnam)
|
||
|
||
fwd = pp.Forward()
|
||
g1 = pp.OneOrMore((pp.Literal("A") + "B" + "C") | fwd)
|
||
g2 = ("C" + g1)[...]
|
||
fwd <<= pp.Group(g2)
|
||
testValidation(fwd, "fwd", isValid=True)
|
||
|
||
fwd2 = pp.Forward()
|
||
fwd2 <<= pp.Group("A" | fwd2)
|
||
testValidation(fwd2, "fwd2", isValid=False)
|
||
|
||
fwd3 = pp.Forward()
|
||
fwd3 <<= pp.Optional("A") + fwd3
|
||
testValidation(fwd3, "fwd3", isValid=False)
|
||
|
||
def testGetNameBehavior(self):
|
||
# test get_name
|
||
print("verify behavior of get_name()")
|
||
aaa = pp.Group(pp.Word("a")("A"))
|
||
bbb = pp.Group(pp.Word("b")("B"))
|
||
ccc = pp.Group(":" + pp.Word("c")("C"))
|
||
g1 = "XXX" + (aaa | bbb | ccc)[...]
|
||
teststring = "XXX b bb a bbb bbbb aa bbbbb :c bbbbbb aaa"
|
||
names = []
|
||
print(g1.parse_string(teststring, parse_all=True).dump())
|
||
for t in g1.parse_string(teststring, parse_all=True):
|
||
print(t, repr(t))
|
||
try:
|
||
names.append(t[0].get_name())
|
||
except Exception:
|
||
try:
|
||
names.append(t.get_name())
|
||
except Exception:
|
||
names.append(None)
|
||
print(teststring)
|
||
print(names)
|
||
self.assertEqual(
|
||
[None, "B", "B", "A", "B", "B", "A", "B", None, "B", "A"],
|
||
names,
|
||
"failure in getting names for tokens",
|
||
)
|
||
|
||
IF, AND, BUT = map(pp.Keyword, "if and but".split())
|
||
ident = ~(IF | AND | BUT) + pp.Word(pp.alphas)("non-key")
|
||
scanner = pp.OneOrMore(IF | AND | BUT | ident)
|
||
|
||
def getNameTester(s, l, t):
|
||
print(t, t.get_name())
|
||
|
||
ident.add_parse_action(getNameTester)
|
||
scanner.parse_string("lsjd sldkjf IF Saslkj AND lsdjf", parse_all=True)
|
||
|
||
# test ParseResults.get() method
|
||
print("verify behavior of ParseResults.get()")
|
||
# use sum() to merge separate groups into single ParseResults
|
||
res = sum(g1.parse_string(teststring, parse_all=True)[1:])
|
||
print(res.dump())
|
||
print(res.get("A", "A not found"))
|
||
print(res.get("D", "!D"))
|
||
self.assertEqual(
|
||
"aaa", res.get("A", "A not found"), "get on existing key failed"
|
||
)
|
||
self.assertEqual("!D", res.get("D", "!D"), "get on missing key failed")
|
||
|
||
def testOptionalBeyondEndOfString(self):
|
||
print("verify handling of Optional's beyond the end of string")
|
||
testGrammar = "A" + pp.Optional("B") + pp.Optional("C") + pp.Optional("D")
|
||
testGrammar.parse_string("A", parse_all=True)
|
||
testGrammar.parse_string("AB", parse_all=True)
|
||
|
||
def testCreateLiteralWithEmptyString(self):
|
||
# test creating Literal with empty string
|
||
print('verify that Literal("") is optimized to Empty()')
|
||
e = pp.Literal("")
|
||
self.assertIsInstance(e, pp.Empty)
|
||
|
||
def testLineMethodSpecialCaseAtStart(self):
|
||
# test line() behavior when starting at 0 and the opening line is an \n
|
||
print("verify correct line() behavior when first line is empty string")
|
||
self.assertEqual(
|
||
"",
|
||
pp.line(0, "\nabc\ndef\n"),
|
||
"Error in line() with empty first line in text",
|
||
)
|
||
txt = "\nabc\ndef\n"
|
||
results = [pp.line(i, txt) for i in range(len(txt))]
|
||
self.assertEqual(
|
||
["", "abc", "abc", "abc", "abc", "def", "def", "def", "def"],
|
||
results,
|
||
"Error in line() with empty first line in text",
|
||
)
|
||
txt = "abc\ndef\n"
|
||
results = [pp.line(i, txt) for i in range(len(txt))]
|
||
self.assertEqual(
|
||
["abc", "abc", "abc", "abc", "def", "def", "def", "def"],
|
||
results,
|
||
"Error in line() with non-empty first line in text",
|
||
)
|
||
|
||
def testRepeatedTokensWhenPackratting(self):
|
||
# test bugfix with repeated tokens when packrat parsing enabled
|
||
print("verify behavior with repeated tokens when packrat parsing is enabled")
|
||
a = pp.Literal("a")
|
||
b = pp.Literal("b")
|
||
c = pp.Literal("c")
|
||
|
||
abb = a + b + b
|
||
abc = a + b + c
|
||
aba = a + b + a
|
||
grammar = abb | abc | aba
|
||
|
||
self.assertEqual(
|
||
"aba",
|
||
"".join(grammar.parse_string("aba", parse_all=True)),
|
||
"Packrat ABA failure!",
|
||
)
|
||
|
||
def testSetResultsNameWithOneOrMoreAndZeroOrMore(self):
|
||
print("verify behavior of set_results_name with OneOrMore and ZeroOrMore")
|
||
stmt = pp.Keyword("test")
|
||
print(stmt[...]("tests").parse_string("test test", parse_all=True).tests)
|
||
print(stmt[1, ...]("tests").parse_string("test test", parse_all=True).tests)
|
||
print(
|
||
pp.Optional(stmt[1, ...]("tests"))
|
||
.parse_string("test test", parse_all=True)
|
||
.tests
|
||
)
|
||
print(
|
||
pp.Optional(stmt[1, ...])("tests")
|
||
.parse_string("test test", parse_all=True)
|
||
.tests
|
||
)
|
||
print(
|
||
pp.Optional(pp.DelimitedList(stmt))("tests")
|
||
.parse_string("test,test", parse_all=True)
|
||
.tests
|
||
)
|
||
self.assertEqual(
|
||
2,
|
||
len(stmt[...]("tests").parse_string("test test", parse_all=True).tests),
|
||
"ZeroOrMore failure with set_results_name",
|
||
)
|
||
self.assertEqual(
|
||
2,
|
||
len(stmt[1, ...]("tests").parse_string("test test", parse_all=True).tests),
|
||
"OneOrMore failure with set_results_name",
|
||
)
|
||
self.assertEqual(
|
||
2,
|
||
len(
|
||
pp.Optional(stmt[1, ...]("tests"))
|
||
.parse_string("test test", parse_all=True)
|
||
.tests
|
||
),
|
||
"OneOrMore failure with set_results_name",
|
||
)
|
||
self.assertEqual(
|
||
2,
|
||
len(
|
||
pp.Optional(pp.DelimitedList(stmt))("tests")
|
||
.parse_string("test,test", parse_all=True)
|
||
.tests
|
||
),
|
||
"DelimitedList failure with set_results_name",
|
||
)
|
||
self.assertEqual(
|
||
2,
|
||
len((stmt * 2)("tests").parse_string("test test", parse_all=True).tests),
|
||
"multiplied(1) failure with set_results_name",
|
||
)
|
||
self.assertEqual(
|
||
2,
|
||
len(stmt[..., 2]("tests").parse_string("test test", parse_all=True).tests),
|
||
"multiplied(2) failure with set_results_name",
|
||
)
|
||
self.assertEqual(
|
||
2,
|
||
len(stmt[1, ...]("tests").parse_string("test test", parse_all=True).tests),
|
||
"multiplied(3) failure with set_results_name",
|
||
)
|
||
self.assertEqual(
|
||
2,
|
||
len(stmt[2, ...]("tests").parse_string("test test", parse_all=True).tests),
|
||
"multiplied(3) failure with set_results_name",
|
||
)
|
||
|
||
def testParseResultsReprWithResultsNames(self):
|
||
word = pp.Word(pp.printables)("word")
|
||
res = word[...].parse_string("test blub", parse_all=True)
|
||
|
||
print(repr(res))
|
||
print(res["word"])
|
||
print(res.as_dict())
|
||
|
||
self.assertEqual(
|
||
"ParseResults(['test', 'blub'], {'word': 'blub'})",
|
||
repr(res),
|
||
"incorrect repr for ParseResults with list_all_matches=False",
|
||
)
|
||
|
||
word = pp.Word(pp.printables)("word*")
|
||
res = word[...].parse_string("test blub", parse_all=True)
|
||
|
||
print(repr(res))
|
||
print(res["word"])
|
||
print(res.as_dict())
|
||
|
||
self.assertEqual(
|
||
"ParseResults(['test', 'blub'], {'word': ['test', 'blub']})",
|
||
repr(res),
|
||
"incorrect repr for ParseResults with list_all_matches=True",
|
||
)
|
||
|
||
def testWarnUsingLshiftForward(self):
|
||
print(
|
||
"verify that using '<<' operator with a Forward raises a warning if there is a dangling '|' operator"
|
||
)
|
||
|
||
fwd = pp.Forward()
|
||
print("unsafe << and |, but diag not enabled, should not warn")
|
||
fwd << pp.Word("a") | pp.Word("b")
|
||
|
||
pp.enable_diag(pp.Diagnostics.warn_on_match_first_with_lshift_operator)
|
||
with self.assertWarns(
|
||
UserWarning, msg="failed to warn of using << and | operators"
|
||
):
|
||
fwd = pp.Forward()
|
||
print("unsafe << and |, should warn")
|
||
fwd << pp.Word("a") | pp.Word("b")
|
||
|
||
with self.assertWarns(
|
||
UserWarning,
|
||
msg="failed to warn of using << and | operators (within lambda)",
|
||
):
|
||
fwd = pp.Forward()
|
||
print("unsafe << and |, should warn")
|
||
fwd_fn = lambda expr1, expr2: fwd << expr1 | expr2
|
||
fwd_fn(pp.Word("a"), pp.Word("b"))
|
||
|
||
fwd = pp.Forward()
|
||
print("safe <<= and |, should not warn")
|
||
fwd <<= pp.Word("a") | pp.Word("b")
|
||
c = fwd | pp.Word("c")
|
||
|
||
print("safe << and (|), should not warn")
|
||
with self.assertDoesNotWarn(
|
||
"warning raised on safe use of << with Forward and MatchFirst"
|
||
):
|
||
fwd = pp.Forward()
|
||
fwd << (pp.Word("a") | pp.Word("b"))
|
||
c = fwd | pp.Word("c")
|
||
|
||
def testParseExpressionsWithRegex(self):
|
||
from itertools import product
|
||
|
||
match_empty_regex = pp.Regex(r"[a-z]*")
|
||
match_nonempty_regex = pp.Regex(r"[a-z]+")
|
||
|
||
parser_classes = pp.ParseExpression.__subclasses__()
|
||
test_string = "abc def"
|
||
expected = ["abc"]
|
||
for expr, cls in product(
|
||
(match_nonempty_regex, match_empty_regex), parser_classes
|
||
):
|
||
print(expr, cls)
|
||
parser = cls([expr])
|
||
parsed_result = parser.parse_string(test_string, parse_all=False)
|
||
print(parsed_result.dump())
|
||
self.assertParseResultsEquals(parsed_result, expected)
|
||
|
||
for expr, cls in product(
|
||
(match_nonempty_regex, match_empty_regex), (pp.MatchFirst, pp.Or)
|
||
):
|
||
parser = cls([expr, expr])
|
||
print(parser)
|
||
parsed_result = parser.parse_string(test_string, parse_all=False)
|
||
print(parsed_result.dump())
|
||
self.assertParseResultsEquals(parsed_result, expected)
|
||
|
||
def testAssertParseAndCheckDict(self):
|
||
"""test assertParseAndCheckDict in test framework"""
|
||
|
||
expr = pp.Word(pp.alphas)("item") + pp.Word(pp.nums)("qty")
|
||
self.assertParseAndCheckDict(
|
||
expr, "balloon 25", {"item": "balloon", "qty": "25"}
|
||
)
|
||
|
||
exprWithInt = pp.Word(pp.alphas)("item") + ppc.integer("qty")
|
||
self.assertParseAndCheckDict(
|
||
exprWithInt, "rucksack 49", {"item": "rucksack", "qty": 49}
|
||
)
|
||
|
||
def testOnlyOnce(self):
|
||
"""test class OnlyOnce and its reset method"""
|
||
|
||
# use a parse action to compute the sum of the parsed integers,
|
||
# and add it to the end
|
||
def append_sum(tokens):
|
||
tokens.append(sum(map(int, tokens)))
|
||
|
||
pa = pp.OnlyOnce(append_sum)
|
||
expr = pp.OneOrMore(pp.Word(pp.nums)).add_parse_action(pa)
|
||
|
||
result = expr.parse_string("0 123 321", parse_all=True)
|
||
print(result.dump())
|
||
expected = ["0", "123", "321", 444]
|
||
self.assertParseResultsEquals(
|
||
result, expected, msg="issue with OnlyOnce first call"
|
||
)
|
||
|
||
with self.assertRaisesParseException(
|
||
msg="failed to raise exception calling OnlyOnce more than once"
|
||
):
|
||
result2 = expr.parse_string("1 2 3 4 5", parse_all=True)
|
||
|
||
pa.reset()
|
||
result = expr.parse_string("100 200 300")
|
||
print(result.dump())
|
||
expected = ["100", "200", "300", 600]
|
||
self.assertParseResultsEquals(
|
||
result, expected, msg="issue with OnlyOnce after reset"
|
||
)
|
||
|
||
def testGoToColumn(self):
|
||
"""tests for GoToColumn class"""
|
||
|
||
dateExpr = pp.Regex(r"\d\d(\.\d\d){2}")("date")
|
||
numExpr = ppc.number("num")
|
||
|
||
sample = """\
|
||
date Not Important value NotImportant2
|
||
11.11.13 | useless . useless,21 useless 2 | 14.21 | asmdakldm
|
||
21.12.12 | fmpaosmfpoamsp 4 | 41 | ajfa9si90""".splitlines()
|
||
|
||
# Column number finds match
|
||
patt = dateExpr + pp.GoToColumn(70).ignore("|") + numExpr + pp.rest_of_line
|
||
|
||
infile = iter(sample)
|
||
next(infile)
|
||
|
||
expecteds = [["11.11.13", 14.21], ["21.12.12", 41]]
|
||
for line, expected in zip(infile, expecteds):
|
||
result = patt.parse_string(line, parse_all=True)
|
||
print(result)
|
||
|
||
self.assertEqual(
|
||
expected, [result.date, result.num], msg="issue with GoToColumn"
|
||
)
|
||
|
||
# Column number does NOT match
|
||
patt = dateExpr("date") + pp.GoToColumn(30) + numExpr + pp.rest_of_line
|
||
|
||
infile = iter(sample)
|
||
next(infile)
|
||
|
||
for line in infile:
|
||
with self.assertRaisesParseException(
|
||
msg="issue with GoToColumn not finding match"
|
||
):
|
||
result = patt.parse_string(line, parse_all=True)
|
||
|
||
def testExceptionExplainVariations(self):
|
||
class Modifier:
|
||
def modify_upper(self, tokens):
|
||
tokens[:] = map(str.upper, tokens)
|
||
|
||
modder = Modifier()
|
||
|
||
# force an exception in the attached parse action
|
||
# integer has a parse action to convert to an int;
|
||
# this parse action should fail with a TypeError, since
|
||
# str.upper expects a str argument, not an int
|
||
grammar = ppc.integer().add_parse_action(modder.modify_upper)
|
||
|
||
self_testcase_name = "tests.test_unit." + type(self).__name__
|
||
|
||
try:
|
||
grammar.parse_string("1000", parse_all=True)
|
||
except Exception as e:
|
||
# extract the exception explanation
|
||
explain_str = ParseException.explain_exception(e)
|
||
print(explain_str)
|
||
explain_str_lines = explain_str.splitlines()
|
||
|
||
expected = [
|
||
self_testcase_name,
|
||
"pyparsing.core.Word - integer",
|
||
"tests.test_unit.Modifier",
|
||
"pyparsing.results.ParseResults",
|
||
]
|
||
|
||
# verify the list of names shown in the explain "stack"
|
||
self.assertEqual(
|
||
expected, explain_str_lines[-len(expected) :], msg="invalid explain str"
|
||
)
|
||
|
||
# check type of raised exception matches explain output
|
||
# (actual exception text varies by Python version, and even
|
||
# by how the exception is raised, so we can only check the
|
||
# type name)
|
||
exception_line = explain_str_lines[-(len(expected) + 1)]
|
||
self.assertTrue(
|
||
exception_line.startswith("TypeError:"),
|
||
msg=f"unexpected exception line ({exception_line!r})",
|
||
)
|
||
|
||
def testExceptionMessageCustomization(self):
|
||
with resetting(pp.ParseBaseException, "formatted_message"):
|
||
|
||
def custom_exception_message(exc) -> str:
|
||
found_phrase = f", found {exc.found}" if exc.found else ""
|
||
return f"{exc.lineno}:{exc.column} {exc.msg}{found_phrase}"
|
||
|
||
pp.ParseBaseException.formatted_message = custom_exception_message
|
||
|
||
try:
|
||
pp.Word(pp.nums).parse_string("ABC")
|
||
except ParseException as pe:
|
||
pe_msg = str(pe)
|
||
else:
|
||
pe_msg = ""
|
||
|
||
self.assertEqual("1:1 Expected W:(0-9), found 'ABC'", pe_msg)
|
||
|
||
def testForwardReferenceException(self):
|
||
token = pp.Forward()
|
||
num = pp.Word(pp.nums)
|
||
num.set_name("num")
|
||
text = pp.Word(pp.alphas)
|
||
text.set_name("text")
|
||
fail = pp.Regex(r"\\[A-Za-z]*")("name")
|
||
|
||
def parse_fail(s, loc, toks):
|
||
raise pp.ParseFatalException(s, loc, f"Unknown symbol: {toks['name']}")
|
||
|
||
fail.set_parse_action(parse_fail)
|
||
token <<= num | text | fail
|
||
|
||
# If no name is given, do not intercept error messages
|
||
with self.assertRaises(pp.ParseFatalException, msg="Unknown symbol: \\fail"):
|
||
token.parse_string("\\fail")
|
||
|
||
# If name is given, do intercept error messages
|
||
token.set_name("token")
|
||
with self.assertRaises(pp.ParseFatalException, msg="Expected token, found.*"):
|
||
token.parse_string("\\fail")
|
||
|
||
def testForwardExceptionText(self):
|
||
wd = pp.Word(pp.alphas)
|
||
|
||
ff = pp.Forward().set_name("fffff!")
|
||
ff <<= wd + pp.Opt(ff)
|
||
|
||
with self.assertRaises(pp.ParseFatalException, msg="no numbers!"):
|
||
try:
|
||
ff.parse_string("123")
|
||
except pp.ParseException as pe:
|
||
raise pp.ParseSyntaxException("no numbers! just alphas!") from pe
|
||
|
||
with self.assertRaises(pp.ParseException, msg="Expected W:(A-Za-z)"):
|
||
ff2 = pp.Forward()
|
||
ff2 <<= wd
|
||
ff2.parse_string("123")
|
||
|
||
def testForwardExceptionText2(self):
|
||
"""
|
||
Test various expressions for error messages, under conditions in wrapped ParserElements
|
||
"""
|
||
v = "(omit closing paren"
|
||
w = "('omit closing quote)"
|
||
|
||
for s, expr, expected in (
|
||
(v, pp.nested_expr(), "Expected ')'"),
|
||
(v, pp.Combine(pp.nested_expr(), adjacent=False), "Expected ')'"),
|
||
(
|
||
v,
|
||
pp.QuotedString("(", end_quote_char=")"),
|
||
"Expected quoted string, starting with ( ending with ), found '('",
|
||
),
|
||
(w, pp.nested_expr(content=pp.sgl_quoted_string), "Expected ')'"),
|
||
("", pp.nested_expr(), ""),
|
||
("", pp.Word("A"), ""),
|
||
):
|
||
print(repr(s))
|
||
print(expr)
|
||
|
||
with self.subTest("parse expr", expr=expr, s=s, expected=expected):
|
||
with self.assertRaisesParseException(expected_msg=expected) as ctx:
|
||
expr.parse_string(s, parse_all=True)
|
||
print(ctx.exception)
|
||
|
||
with self.subTest("parse expr[1, ...]", expr=expr, s=s, expected=expected):
|
||
with self.assertRaisesParseException(expected_msg=expected) as ctx:
|
||
expr[1, ...].parse_string(s, parse_all=True)
|
||
print(ctx.exception)
|
||
|
||
with self.subTest(
|
||
"parse DelimitedList(expr)", expr=expr, s=s, expected=expected
|
||
):
|
||
with self.assertRaisesParseException(expected_msg=expected) as ctx:
|
||
pp.DelimitedList(expr).parse_string(s, parse_all=True)
|
||
print(ctx.exception)
|
||
|
||
print()
|
||
|
||
def testMiscellaneousExceptionBits(self):
|
||
pp.ParserElement.verbose_stacktrace = True
|
||
|
||
self_testcase_name = "tests.test_unit." + type(self).__name__
|
||
|
||
# force a parsing exception - match an integer against "ABC"
|
||
try:
|
||
pp.Word(pp.nums).parse_string("ABC", parse_all=True)
|
||
except pp.ParseException as pe:
|
||
expected_str = "Expected W:(0-9), found 'ABC' (at char 0), (line:1, col:1)"
|
||
self.assertEqual(expected_str, str(pe), "invalid ParseException str")
|
||
self.assertEqual(expected_str, repr(pe), "invalid ParseException repr")
|
||
|
||
self.assertEqual(
|
||
">!<ABC", pe.mark_input_line(), "invalid default mark input line"
|
||
)
|
||
self.assertEqual(
|
||
"ABC", pe.mark_input_line(""), "invalid mark input line with '' marker"
|
||
)
|
||
|
||
# test explain using depth=None, 0, 1
|
||
depth_none_explain_str = pe.explain(depth=None)
|
||
depth_0_explain_str = pe.explain(depth=0)
|
||
depth_1_explain_str = pe.explain(depth=1)
|
||
print(depth_none_explain_str)
|
||
print()
|
||
print(depth_0_explain_str)
|
||
print()
|
||
print(depth_1_explain_str)
|
||
|
||
expr_name = "pyparsing.core.Word - W:(0-9)"
|
||
for expected_function in [self_testcase_name, expr_name]:
|
||
self.assertTrue(
|
||
expected_function in depth_none_explain_str,
|
||
f"{expected_function!r} not found in ParseException.explain()",
|
||
)
|
||
self.assertFalse(
|
||
expected_function in depth_0_explain_str,
|
||
f"{expected_function!r} found in ParseException.explain(depth=0)",
|
||
)
|
||
|
||
self.assertTrue(
|
||
expr_name in depth_1_explain_str,
|
||
f"{expected_function!r} not found in ParseException.explain()",
|
||
)
|
||
self.assertFalse(
|
||
self_testcase_name in depth_1_explain_str,
|
||
f"{expected_function!r} not found in ParseException.explain()",
|
||
)
|
||
|
||
def testExpressionDefaultStrings(self):
|
||
expr = pp.Word(pp.nums)
|
||
print(expr)
|
||
self.assertEqual("W:(0-9)", repr(expr))
|
||
|
||
expr = pp.Word(pp.nums, exact=3)
|
||
print(expr)
|
||
self.assertEqual("W:(0-9){3}", repr(expr))
|
||
|
||
expr = pp.Word(pp.nums, min=2)
|
||
print(expr)
|
||
self.assertEqual("W:(0-9){2,...}", repr(expr))
|
||
|
||
expr = pp.Word(pp.nums, max=3)
|
||
print(expr)
|
||
self.assertEqual("W:(0-9){1,3}", repr(expr))
|
||
|
||
expr = pp.Word(pp.nums, min=2, max=3)
|
||
print(expr)
|
||
self.assertEqual("W:(0-9){2,3}", repr(expr))
|
||
|
||
expr = pp.Char(pp.nums)
|
||
print(expr)
|
||
self.assertEqual("(0-9)", repr(expr))
|
||
|
||
def testEmptyExpressionsAreHandledProperly(self):
|
||
try:
|
||
from pyparsing.diagram import to_railroad
|
||
except ModuleNotFoundError as mnfe:
|
||
print(
|
||
"Failed 'from pyparsing.diagram import to_railroad'"
|
||
f"\n {type(mnfe).__name__}: {mnfe}"
|
||
)
|
||
if mnfe.__cause__:
|
||
print(f"\n {type(mnfe.__cause__).__name__}: {mnfe.__cause__}")
|
||
self.skipTest("Failed 'from pyparsing.diagram import to_railroad'")
|
||
|
||
for cls in (pp.And, pp.Or, pp.MatchFirst, pp.Each):
|
||
print("testing empty", cls.__name__)
|
||
expr = cls([])
|
||
expr.streamline()
|
||
to_railroad(expr)
|
||
|
||
def testForwardsDoProperStreamlining(self):
|
||
wd = pp.Word(pp.alphas)
|
||
w3 = wd + wd + wd
|
||
# before streamlining, w3 is {{W:(A-Za-z) W:(A-Za-z)} W:(A-Za-z)}
|
||
self.assertIsInstance(w3.exprs[0], pp.And)
|
||
self.assertEqual(len(w3.exprs), 2)
|
||
|
||
ff = pp.Forward()
|
||
ff <<= w3 + pp.Opt(ff)
|
||
# before streamlining, ff is {{{W:(A-Za-z) W:(A-Za-z)} W:(A-Za-z)} [Forward: None]}
|
||
self.assertEqual(len(ff.expr.exprs), 2)
|
||
|
||
ff.streamline()
|
||
|
||
# after streamlining:
|
||
# w3 is {W:(A-Za-z) W:(A-Za-z) W:(A-Za-z)}
|
||
# ff is {W:(A-Za-z) W:(A-Za-z) W:(A-Za-z) [Forward: None]}
|
||
self.assertEqual(len(ff.expr.exprs), 4)
|
||
self.assertEqual(len(w3.exprs), 3)
|
||
|
||
test_exception_messages_tests = (
|
||
(pp.Word(pp.alphas), "123", "Expected W:(A-Za-z), found '123'"),
|
||
(pp.Word(pp.alphas).set_name("word"), "123", "Expected word, found '123'"),
|
||
(
|
||
pp.Group(pp.Word(pp.alphas).set_name("word")),
|
||
"123",
|
||
"Expected word, found '123'",
|
||
),
|
||
(
|
||
pp.OneOrMore(pp.Word(pp.alphas).set_name("word")),
|
||
"123",
|
||
"Expected word, found '123'",
|
||
),
|
||
(
|
||
pp.DelimitedList(pp.Word(pp.alphas).set_name("word")),
|
||
"123",
|
||
"Expected word, found '123'",
|
||
),
|
||
(
|
||
pp.Suppress(pp.Word(pp.alphas).set_name("word")),
|
||
"123",
|
||
"Expected word, found '123'",
|
||
),
|
||
(
|
||
pp.Forward() << pp.Word(pp.alphas).set_name("word"),
|
||
"123",
|
||
"Expected word, found '123'",
|
||
),
|
||
(
|
||
pp.Forward() << pp.Word(pp.alphas),
|
||
"123",
|
||
"Expected W:(A-Za-z), found '123'",
|
||
),
|
||
(
|
||
pp.Group(pp.Word(pp.alphas)),
|
||
"123",
|
||
"Expected W:(A-Za-z), found '123'",
|
||
),
|
||
(
|
||
"prefix" + (pp.Regex("a").set_name("a") | pp.Regex("b").set_name("b")),
|
||
"prefixc",
|
||
"Expected {a | b}, found 'c'",
|
||
),
|
||
(
|
||
"prefix" + (pp.Regex("a").set_name("a") | pp.Regex("b").set_name("b")),
|
||
"prefix c",
|
||
"Expected {a | b}, found 'c'",
|
||
),
|
||
(
|
||
"prefix" + (pp.Regex("a").set_name("a") ^ pp.Regex("b").set_name("b")),
|
||
"prefixc",
|
||
"Expected {a ^ b}, found 'c'",
|
||
),
|
||
(
|
||
"prefix" + (pp.Regex("a").set_name("a") ^ pp.Regex("b").set_name("b")),
|
||
"prefix c",
|
||
"Expected {a ^ b}, found 'c'",
|
||
),
|
||
)
|
||
|
||
def test_exception_messages(self, tests=test_exception_messages_tests):
|
||
for expr, input_str, expected_msg in tests:
|
||
with self.subTest(expr=expr, input_str=input_str):
|
||
with self.assertRaisesParseException(expected_msg=expected_msg):
|
||
expr.parse_string(input_str)
|
||
|
||
def test_exception_messages_with_exception_subclass(self):
|
||
class TooManyRepsException(pp.ParseFatalException):
|
||
pass
|
||
|
||
@pp.trace_parse_action
|
||
def no_more_than_3(t):
|
||
if len(t) > 3:
|
||
raise TooManyRepsException(f"{len(t)} is too many, only 3 allowed")
|
||
|
||
# parse an int followed by no more than 3 words
|
||
parser = pp.Word(pp.nums) + pp.Group(
|
||
pp.Word(pp.alphas)[...].add_parse_action(no_more_than_3)
|
||
)
|
||
|
||
# should succeed
|
||
result = parser.parse_string("1000 abc def ghi")
|
||
print(result.dump())
|
||
|
||
# should raise exception with local exception message
|
||
with self.assertRaisesParseException(
|
||
exc_type=ParseFatalException,
|
||
expected_msg="4 is too many, only 3 allowed",
|
||
msg="wrong exception message",
|
||
) as pe_context:
|
||
result = parser.parse_string("2000 abc def ghi jkl")
|
||
|
||
print(pe_context.exception)
|
||
|
||
def test_pep8_synonyms(self):
|
||
"""
|
||
Test that staticmethods wrapped by replaced_by_pep8 wrapper are properly
|
||
callable as staticmethods.
|
||
"""
|
||
|
||
def run_subtest(fn_name, expr=None, args=""):
|
||
bool_expr = pp.one_of("true false", as_keyword=True)
|
||
if expr is None:
|
||
expr = "bool_expr"
|
||
|
||
# try calling a ParserElement staticmethod via a ParserElement instance
|
||
with self.subTest(fn_name=fn_name):
|
||
exec(f"{expr}.{fn_name}({args})", globals(), locals())
|
||
|
||
# access staticmethod synonyms using a ParserElement
|
||
parser_element_staticmethod_names = """
|
||
enable_packrat disable_memoization enable_left_recursion reset_cache
|
||
""".split()
|
||
|
||
if not (
|
||
pp.ParserElement._packratEnabled or pp.ParserElement._left_recursion_enabled
|
||
):
|
||
for name in parser_element_staticmethod_names:
|
||
run_subtest(name)
|
||
pp.ParserElement.disable_memoization()
|
||
|
||
run_subtest("set_default_whitespace_chars", args="' '")
|
||
run_subtest("inline_literals_using", args="pp.Suppress")
|
||
|
||
run_subtest(
|
||
"set_default_keyword_chars", expr="pp.Keyword('START')", args="'abcde'"
|
||
)
|
||
pass
|
||
|
||
|
||
class Test04_WithPackrat(Test02_WithoutPackrat):
|
||
"""
|
||
rerun Test2 tests, now that packrat is enabled
|
||
"""
|
||
def setUp(self):
|
||
ParserElement.enable_packrat(force=True)
|
||
|
||
def tearDown(self):
|
||
default_suite_context.restore()
|
||
|
||
def test000_assert_packrat_status(self):
|
||
print("Packrat enabled:", ParserElement._packratEnabled)
|
||
print(
|
||
"Packrat cache:",
|
||
type(ParserElement.packrat_cache).__name__,
|
||
getattr(ParserElement.packrat_cache, "size", "- no size attribute -"),
|
||
)
|
||
self.assertTrue(ParserElement._packratEnabled, "packrat not enabled")
|
||
self.assertEqual(
|
||
"_FifoCache",
|
||
type(ParserElement.packrat_cache).__name__,
|
||
msg="incorrect cache type",
|
||
)
|
||
|
||
|
||
class Test06_WithBoundedPackrat(Test02_WithoutPackrat):
|
||
"""
|
||
rerun Test2 tests, now with bounded packrat cache
|
||
"""
|
||
def setUp(self):
|
||
ParserElement.enable_packrat(cache_size_limit=16, force=True)
|
||
|
||
def tearDown(self):
|
||
default_suite_context.restore()
|
||
|
||
def test000_assert_packrat_status(self):
|
||
print("Packrat enabled:", ParserElement._packratEnabled)
|
||
print(
|
||
"Packrat cache:",
|
||
type(ParserElement.packrat_cache).__name__,
|
||
getattr(ParserElement.packrat_cache, "size", "- no size attribute -"),
|
||
)
|
||
self.assertTrue(ParserElement._packratEnabled, "packrat not enabled")
|
||
self.assertEqual(
|
||
"_FifoCache",
|
||
type(ParserElement.packrat_cache).__name__,
|
||
msg="incorrect cache type",
|
||
)
|
||
|
||
def test_exceeding_fifo_cache_size(self):
|
||
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||
letter_lit = pp.MatchFirst(pp.Literal.using_each(letters))
|
||
result = letter_lit[...].parse_string(letters, parse_all=True)
|
||
self.assertEqual(list(result), list(letters))
|
||
|
||
|
||
class Test08_WithUnboundedPackrat(Test02_WithoutPackrat):
|
||
"""
|
||
rerun Test2 tests, now with unbounded packrat cache
|
||
"""
|
||
def setUp(self):
|
||
ParserElement.enable_packrat(cache_size_limit=None, force=True)
|
||
|
||
def tearDown(self):
|
||
default_suite_context.restore()
|
||
|
||
|
||
def test000_assert_packrat_status(self):
|
||
print("Packrat enabled:", ParserElement._packratEnabled)
|
||
print(
|
||
"Packrat cache:",
|
||
type(ParserElement.packrat_cache).__name__,
|
||
getattr(ParserElement.packrat_cache, "size", "- no size attribute -"),
|
||
)
|
||
self.assertTrue(ParserElement._packratEnabled, "packrat not enabled")
|
||
self.assertEqual(
|
||
"_UnboundedCache",
|
||
type(ParserElement.packrat_cache).__name__,
|
||
msg="incorrect cache type",
|
||
)
|
||
|
||
|
||
class Test09_WithLeftRecursionParsing(Test02_WithoutPackrat):
|
||
"""
|
||
rerun Test2 tests, now with unbounded left recursion cache
|
||
"""
|
||
|
||
def setUp(self):
|
||
ParserElement.enable_left_recursion(force=True)
|
||
|
||
def tearDown(self):
|
||
default_suite_context.restore()
|
||
|
||
def test000_assert_packrat_status(self):
|
||
print("Left-Recursion enabled:", ParserElement._left_recursion_enabled)
|
||
self.assertTrue(
|
||
ParserElement._left_recursion_enabled, "left recursion not enabled"
|
||
)
|
||
self.assertIsInstance(ParserElement.recursion_memos, pp.util.UnboundedMemo)
|
||
|
||
|
||
class Test10_WithLeftRecursionParsingBoundedMemo(Test02_WithoutPackrat):
|
||
"""
|
||
rerun Test2 tests, now with bounded left recursion cache
|
||
"""
|
||
|
||
def setUp(self):
|
||
ParserElement.enable_left_recursion(cache_size_limit=4, force=True)
|
||
|
||
def tearDown(self):
|
||
default_suite_context.restore()
|
||
|
||
def test000_assert_packrat_status(self):
|
||
print("Left-Recursion enabled:", ParserElement._left_recursion_enabled)
|
||
self.assertTrue(
|
||
ParserElement._left_recursion_enabled, "left recursion not enabled"
|
||
)
|
||
self.assertIsInstance(ParserElement.recursion_memos, pp.util.LRUMemo)
|
||
# check that the cache matches roughly what we expect
|
||
# – it may be larger due to action handling
|
||
self.assertLessEqual(ParserElement.recursion_memos._capacity, 4)
|
||
self.assertGreater(ParserElement.recursion_memos._capacity * 3, 4)
|
||
|
||
|
||
class Test11_LR1_Recursion(ppt.TestParseResultsAsserts, TestCase):
|
||
"""
|
||
Tests for recursive parsing
|
||
"""
|
||
|
||
suite_context = None
|
||
save_suite_context = None
|
||
|
||
def setUp(self):
|
||
recursion_suite_context.restore()
|
||
|
||
def tearDown(self):
|
||
default_suite_context.restore()
|
||
|
||
def test_repeat_as_recurse(self):
|
||
"""repetition rules formulated with recursion"""
|
||
one_or_more = pp.Forward().set_name("one_or_more")
|
||
one_or_more <<= one_or_more + "a" | "a"
|
||
self.assertParseResultsEquals(
|
||
one_or_more.parse_string("a", parse_all=True), expected_list=["a"]
|
||
)
|
||
self.assertParseResultsEquals(
|
||
one_or_more.parse_string("aaa aa", parse_all=True),
|
||
expected_list=["a", "a", "a", "a", "a"],
|
||
)
|
||
DelimitedList = pp.Forward().set_name("DelimitedList")
|
||
DelimitedList <<= DelimitedList + pp.Suppress(",") + "b" | "b"
|
||
self.assertParseResultsEquals(
|
||
DelimitedList.parse_string("b", parse_all=True), expected_list=["b"]
|
||
)
|
||
self.assertParseResultsEquals(
|
||
DelimitedList.parse_string("b,b", parse_all=True), expected_list=["b", "b"]
|
||
)
|
||
self.assertParseResultsEquals(
|
||
DelimitedList.parse_string("b,b , b, b,b", parse_all=True),
|
||
expected_list=["b", "b", "b", "b", "b"],
|
||
)
|
||
|
||
def test_binary_recursive(self):
|
||
"""parsing of single left-recursive binary operator"""
|
||
expr = pp.Forward().set_name("expr")
|
||
num = pp.Word(pp.nums)
|
||
expr <<= expr + "+" - num | num
|
||
self.assertParseResultsEquals(
|
||
expr.parse_string("1+2", parse_all=True), expected_list=["1", "+", "2"]
|
||
)
|
||
self.assertParseResultsEquals(
|
||
expr.parse_string("1+2+3+4", parse_all=True),
|
||
expected_list=["1", "+", "2", "+", "3", "+", "4"],
|
||
)
|
||
|
||
def test_binary_associative(self):
|
||
"""associative is preserved for single left-recursive binary operator"""
|
||
expr = pp.Forward().set_name("expr")
|
||
num = pp.Word(pp.nums)
|
||
expr <<= pp.Group(expr) + "+" - num | num
|
||
self.assertParseResultsEquals(
|
||
expr.parse_string("1+2", parse_all=True), expected_list=[["1"], "+", "2"]
|
||
)
|
||
self.assertParseResultsEquals(
|
||
expr.parse_string("1+2+3+4", parse_all=True),
|
||
expected_list=[[[["1"], "+", "2"], "+", "3"], "+", "4"],
|
||
)
|
||
|
||
def test_add_sub(self):
|
||
"""indirectly left-recursive/associative add/sub calculator"""
|
||
expr = pp.Forward().set_name("expr")
|
||
num = pp.Word(pp.nums).set_parse_action(lambda t: int(t[0]))
|
||
expr <<= (
|
||
(expr + "+" - num).set_parse_action(lambda t: t[0] + t[2])
|
||
| (expr + "-" - num).set_parse_action(lambda t: t[0] - t[2])
|
||
| num
|
||
)
|
||
self.assertEqual(expr.parse_string("1+2", parse_all=True)[0], 3)
|
||
self.assertEqual(expr.parse_string("1+2+3", parse_all=True)[0], 6)
|
||
self.assertEqual(expr.parse_string("1+2-3", parse_all=True)[0], 0)
|
||
self.assertEqual(expr.parse_string("1-2+3", parse_all=True)[0], 2)
|
||
self.assertEqual(expr.parse_string("1-2-3", parse_all=True)[0], -4)
|
||
|
||
def test_math(self):
|
||
"""precedence climbing parser for math"""
|
||
# named references
|
||
expr = pp.Forward().set_name("expr")
|
||
add_sub = pp.Forward().set_name("add_sub")
|
||
mul_div = pp.Forward().set_name("mul_div")
|
||
power = pp.Forward().set_name("power")
|
||
terminal = pp.Forward().set_name("terminal")
|
||
# concrete rules
|
||
number = pp.Word(pp.nums).set_parse_action(lambda t: int(t[0]))
|
||
signed = ("+" - expr) | ("-" - expr).set_parse_action(lambda t: -t[1])
|
||
group = pp.Suppress("(") - expr - pp.Suppress(")")
|
||
add_sub <<= (
|
||
(add_sub + "+" - mul_div).set_parse_action(lambda t: t[0] + t[2])
|
||
| (add_sub + "-" - mul_div).set_parse_action(lambda t: t[0] - t[2])
|
||
| mul_div
|
||
)
|
||
mul_div <<= (
|
||
(mul_div + "*" - power).set_parse_action(lambda t: t[0] * t[2])
|
||
| (mul_div + "/" - power).set_parse_action(lambda t: t[0] / t[2])
|
||
| power
|
||
)
|
||
power <<= (terminal + "^" - power).set_parse_action(
|
||
lambda t: t[0] ** t[2]
|
||
) | terminal
|
||
terminal <<= number | signed | group
|
||
expr <<= add_sub
|
||
# simple add_sub expressions
|
||
self.assertEqual(expr.parse_string("1+2", parse_all=True)[0], 3)
|
||
self.assertEqual(expr.parse_string("1+2+3", parse_all=True)[0], 6)
|
||
self.assertEqual(expr.parse_string("1+2-3", parse_all=True)[0], 0)
|
||
self.assertEqual(expr.parse_string("1-2+3", parse_all=True)[0], 2)
|
||
self.assertEqual(expr.parse_string("1-2-3", parse_all=True)[0], -4)
|
||
# precedence overwriting via parentheses
|
||
self.assertEqual(expr.parse_string("1+(2+3)", parse_all=True)[0], 6)
|
||
self.assertEqual(expr.parse_string("1+(2-3)", parse_all=True)[0], 0)
|
||
self.assertEqual(expr.parse_string("1-(2+3)", parse_all=True)[0], -4)
|
||
self.assertEqual(expr.parse_string("1-(2-3)", parse_all=True)[0], 2)
|
||
# complicated math expressions – same as Python expressions
|
||
self.assertEqual(expr.parse_string("1----3", parse_all=True)[0], 1 - ---3)
|
||
self.assertEqual(expr.parse_string("1+2*3", parse_all=True)[0], 1 + 2 * 3)
|
||
self.assertEqual(expr.parse_string("1*2+3", parse_all=True)[0], 1 * 2 + 3)
|
||
self.assertEqual(expr.parse_string("1*2^3", parse_all=True)[0], 1 * 2**3)
|
||
self.assertEqual(expr.parse_string("4^3^2^1", parse_all=True)[0], 4**3**2**1)
|
||
|
||
def test_terminate_empty(self):
|
||
"""Recursion with ``Empty`` terminates"""
|
||
empty = pp.Forward().set_name("e")
|
||
empty <<= empty + pp.Empty() | pp.Empty()
|
||
self.assertParseResultsEquals(
|
||
empty.parse_string("", parse_all=True), expected_list=[]
|
||
)
|
||
|
||
def test_non_peg(self):
|
||
"""Recursion works for non-PEG operators"""
|
||
expr = pp.Forward().set_name("expr")
|
||
expr <<= expr + "a" ^ expr + "ab" ^ expr + "abc" ^ "."
|
||
self.assertParseResultsEquals(
|
||
expr.parse_string(".abcabaabc", parse_all=True),
|
||
expected_list=[".", "abc", "ab", "a", "abc"],
|
||
)
|
||
|
||
|
||
class TestShowBestPractices(unittest.TestCase):
|
||
def test_loads_markdown_file(self):
|
||
# Mock the file read to simulate the Markdown content
|
||
mock_content = "## Test Best Practices\n- Example guideline"
|
||
mock_file = mock_open(read_data=mock_content)
|
||
|
||
with patch("importlib.resources.files") as mock_files:
|
||
# mock path.open() to use our mock_file
|
||
mock_path = mock_files.return_value.joinpath.return_value
|
||
mock_path.open = mock_file
|
||
|
||
result = pp.show_best_practices(file=None)
|
||
|
||
self.assertEqual(result, mock_content)
|
||
|
||
def test_fallback_when_file_missing(self):
|
||
# Patch files().joinpath().open to raise FileNotFoundError
|
||
with patch("importlib.resources.files") as mock_files:
|
||
mock_path = mock_files.return_value.joinpath.return_value
|
||
mock_path.open.side_effect = FileNotFoundError
|
||
|
||
result = pp.show_best_practices(file=None)
|
||
|
||
self.assertIn("## Planning", result) # Fallback contains "Planning" section
|
||
self.assertIn("## Implementing", result)
|
||
self.assertIn("## Testing", result)
|
||
self.assertIn("## Debugging", result)
|
||
|
||
def test_cli_invocation_with_module_flag(self):
|
||
# Invoke the CLI the same way a user or AI would:
|
||
# python -m pyparsing.show_best_practices
|
||
cmd = [sys.executable, "-m", "pyparsing.ai.show_best_practices"]
|
||
subproc = subprocess.run(cmd, capture_output=True, text=True)
|
||
|
||
self.assertEqual(subproc.returncode, 0, msg=f"stderr: {subproc.stderr}")
|
||
|
||
subproc_stdout = subproc.stdout
|
||
# Should print the best practices markdown (either from file or fallback)
|
||
self.assertIn("## Planning", subproc_stdout)
|
||
self.assertIn("## Implementing", subproc_stdout)
|
||
self.assertIn("## Testing", subproc_stdout)
|
||
self.assertIn("## Debugging", subproc_stdout)
|
||
|
||
|
||
# force clear of packrat parsing flags before saving contexts
|
||
pp.ParserElement.disable_memoization()
|
||
|
||
Test02_WithoutPackrat.suite_context = ppt.reset_pyparsing_context().save()
|
||
Test02_WithoutPackrat.save_suite_context = ppt.reset_pyparsing_context().save()
|
||
|
||
default_suite_context = ppt.reset_pyparsing_context().save()
|
||
pp.ParserElement.enable_left_recursion()
|
||
recursion_suite_context = ppt.reset_pyparsing_context().save()
|
||
default_suite_context.restore()
|