Change Log
==========
+Version 3.0.6 -
+---------------
+- Added `suppress_warning()` method to individually suppress a warning on a
+ specific ParserElement. Used to refactor `original_text_for` to preserve
+ internal results names, which, while undocumented, had been adopted by
+ some projects.
+
+- Fix bug when `delimited_list` was called with a str literal instead of a
+ parse expression.
+
+
Version 3.0.5 -
---------------
- Added return type annotations for `col`, `line`, and `lineno`.
Metadata-Version: 2.1
Name: pyparsing
-Version: 3.0.5
+Version: 3.0.6
Summary: Python parsing module
Home-page: https://github.com/pyparsing/pyparsing/
Author: Paul McGuire
Metadata-Version: 2.1
Name: pyparsing
-Version: 3.0.5
+Version: 3.0.6
Summary: Python parsing module
Home-page: https://github.com/pyparsing/pyparsing/
Author: Paul McGuire
- find more useful common expressions in the :class:`pyparsing_common`
namespace class
"""
-from collections import namedtuple
+from typing import NamedTuple
-version_info = namedtuple("version_info", "major minor micro release_level serial")
-__version_info__ = version_info(3, 0, 5, "final", 0)
-__version__ = "{}.{}.{}".format(*__version_info__[:3]) + (
- "{}{}{}".format(
- "r" if __version_info__.release_level[0] == "c" else "",
- __version_info__.release_level[0],
- __version_info__.serial,
- ),
- "",
-)[__version_info__.release_level == "final"]
-__version_time__ = "5 November 2021 10:05 UTC"
-version_info.__str__ = lambda *args: "pyparsing {} - {}".format(
- __version__, __version_time__
-)
+
+class version_info(NamedTuple):
+ major: int
+ minor: int
+ micro: int
+ releaselevel: str
+ serial: int
+
+ @property
+ def __version__(self):
+ return "{}.{}.{}".format(self.major, self.minor, self.micro) + (
+ "{}{}{}".format(
+ "r" if self.releaselevel[0] == "c" else "",
+ self.releaselevel[0],
+ self.serial,
+ ),
+ "",
+ )[self.releaselevel == "final"]
+
+ def __str__(self):
+ return "{} {} / {}".format(__name__, self.__version__, __version_time__)
+
+ def __repr__(self):
+ return "{}.{}({})".format(
+ __name__,
+ type(self).__name__,
+ ", ".join("{}={!r}".format(*nv) for nv in zip(self._fields, self)),
+ )
+
+
+__version_info__ = version_info(3, 0, 6, "final", 0)
+__version_time__ = "12 Nov 2021 16:06 UTC"
+__version__ = __version_info__.__version__
__versionTime__ = __version_time__
__author__ = "Paul McGuire <ptmcg.gm+pyparsing@gmail.com>"
# avoid redundant calls to preParse
self.callPreparse = True
self.callDuringTry = False
+ self.suppress_warnings_ = []
+
+ def suppress_warning(self, warning_type: Diagnostics):
+ """
+ Suppress warnings emitted for a particular diagnostic on this expression.
+
+ Example::
+
+ base = pp.Forward()
+ base.suppress_warning(Diagnostics.warn_on_parse_using_empty_Forward)
+
+ # statement would normally raise a warning, but is now suppressed
+ print(base.parseString("x"))
+
+ """
+ self.suppress_warnings_.append(warning_type)
+ return self
def copy(self) -> "ParserElement":
"""
max_matches: int = _MAX_INT,
overlap: bool = False,
*,
+ debug: bool = False,
maxMatches: int = _MAX_INT,
) -> Generator[Tuple[ParseResults, int, int], None, None]:
"""
else:
if nextLoc > loc:
matches += 1
+ if debug:
+ print(
+ {
+ "tokens": tokens.asList(),
+ "start": preloc,
+ "end": nextLoc,
+ }
+ )
yield tokens, preloc, nextLoc
if overlap:
nextloc = preparseFn(instring, loc)
# catch and re-raise exception from here, clears out pyparsing internal stack trace
raise exc.with_traceback(None)
- def transform_string(self, instring: str) -> str:
+ def transform_string(self, instring: str, *, debug: bool = False) -> str:
"""
Extension to :class:`scan_string`, to modify matching text with modified tokens that may
be returned from a parse action. To use ``transform_string``, define a grammar and
# keep string locs straight between transform_string and scan_string
self.keepTabs = True
try:
- for t, s, e in self.scan_string(instring):
+ for t, s, e in self.scan_string(instring, debug=debug):
out.append(instring[lastE:s])
if t:
if isinstance(t, ParseResults):
raise exc.with_traceback(None)
def search_string(
- self, instring: str, max_matches: int = _MAX_INT, *, maxMatches: int = _MAX_INT
+ self,
+ instring: str,
+ max_matches: int = _MAX_INT,
+ *,
+ debug: bool = False,
+ maxMatches: int = _MAX_INT,
) -> ParseResults:
"""
Another extension to :class:`scan_string`, simplifying the access to the tokens found
maxMatches = min(maxMatches, max_matches)
try:
return ParseResults(
- [t for t, s, e in self.scan_string(instring, maxMatches)]
+ [t for t, s, e in self.scan_string(instring, maxMatches, debug=debug)]
)
except ParseBaseException as exc:
if ParserElement.verbose_stacktrace:
fatal = "(FATAL)" if isinstance(pe, ParseFatalException) else ""
out.append(pe.explain())
out.append("FAIL: " + str(pe))
+ if ParserElement.verbose_stacktrace:
+ out.extend(traceback.format_tb(pe.__traceback__))
success = success and failureTests
result = pe
except Exception as exc:
- out.append("FAIL-EXCEPTION: " + str(exc))
+ out.append("FAIL-EXCEPTION: {}: {}".format(type(exc).__name__, exc))
+ if ParserElement.verbose_stacktrace:
+ out.extend(traceback.format_tb(exc.__traceback__))
success = success and failureTests
result = exc
else:
return ret
def _setResultsName(self, name, listAllMatches=False):
- if __diag__.warn_ungrouped_named_tokens_in_collection:
+ if (
+ __diag__.warn_ungrouped_named_tokens_in_collection
+ and Diagnostics.warn_ungrouped_named_tokens_in_collection
+ not in self.suppress_warnings_
+ ):
for e in self.exprs:
if (
isinstance(e, ParserElement)
and e.resultsName
- and not e.resultsName.startswith("_NOWARN")
+ and Diagnostics.warn_ungrouped_named_tokens_in_collection
+ not in e.suppress_warnings_
):
warnings.warn(
"{}: setting results name {!r} on {} expression "
return "{" + " ^ ".join(str(e) for e in self.exprs) + "}"
def _setResultsName(self, name, listAllMatches=False):
- if __diag__.warn_multiple_tokens_in_named_alternation:
- if any(isinstance(e, And) for e in self.exprs):
+ if (
+ __diag__.warn_multiple_tokens_in_named_alternation
+ and Diagnostics.warn_multiple_tokens_in_named_alternation
+ not in self.suppress_warnings_
+ ):
+ if any(
+ isinstance(e, And)
+ and Diagnostics.warn_multiple_tokens_in_named_alternation
+ not in e.suppress_warnings_
+ for e in self.exprs
+ ):
warnings.warn(
"{}: setting results name {!r} on {} expression "
"will return a list of all parsed tokens in an And alternative, "
- "in prior versions only the first token was returned".format(
+ "in prior versions only the first token was returned; enclose"
+ "contained argument in Group".format(
"warn_multiple_tokens_in_named_alternation",
name,
type(self).__name__,
return "{" + " | ".join(str(e) for e in self.exprs) + "}"
def _setResultsName(self, name, listAllMatches=False):
- if __diag__.warn_multiple_tokens_in_named_alternation:
- if any(isinstance(e, And) for e in self.exprs):
+ if (
+ __diag__.warn_multiple_tokens_in_named_alternation
+ and Diagnostics.warn_multiple_tokens_in_named_alternation
+ not in self.suppress_warnings_
+ ):
+ if any(
+ isinstance(e, And)
+ and Diagnostics.warn_multiple_tokens_in_named_alternation
+ not in e.suppress_warnings_
+ for e in self.exprs
+ ):
warnings.warn(
"{}: setting results name {!r} on {} expression "
- "may only return a single token for an And alternative, "
- "in future will return the full list of tokens".format(
+ "will return a list of all parsed tokens in an And alternative, "
+ "in prior versions only the first token was returned; enclose"
+ "contained argument in Group".format(
"warn_multiple_tokens_in_named_alternation",
name,
type(self).__name__,
return loc, tokens
def _setResultsName(self, name, listAllMatches=False):
- if __diag__.warn_ungrouped_named_tokens_in_collection:
+ if (
+ __diag__.warn_ungrouped_named_tokens_in_collection
+ and Diagnostics.warn_ungrouped_named_tokens_in_collection
+ not in self.suppress_warnings_
+ ):
for e in [self.expr] + self.expr.recurse():
if (
isinstance(e, ParserElement)
and e.resultsName
- and not e.resultsName.startswith("_NOWARN")
+ and Diagnostics.warn_ungrouped_named_tokens_in_collection
+ not in e.suppress_warnings_
):
warnings.warn(
"{}: setting results name {!r} on {} expression "
if (
__diag__.warn_on_match_first_with_lshift_operator
and caller_line == self.lshift_line
+ and Diagnostics.warn_on_match_first_with_lshift_operator
+ not in self.suppress_warnings_
):
warnings.warn(
"using '<<' operator with '|' is probably an error, use '<<='",
def __del__(self):
# see if we are getting dropped because of '=' reassignment of var instead of '<<=' or '<<'
- if self.expr is None and __diag__.warn_on_assignment_to_Forward:
+ if (
+ self.expr is None
+ and __diag__.warn_on_assignment_to_Forward
+ and Diagnostics.warn_on_assignment_to_Forward not in self.suppress_warnings_
+ ):
warnings.warn_explicit(
"Forward defined here but no expression attached later using '<<=' or '<<'",
UserWarning,
)
def parseImpl(self, instring, loc, doActions=True):
- if self.expr is None and __diag__.warn_on_parse_using_empty_Forward:
+ if (
+ self.expr is None
+ and __diag__.warn_on_parse_using_empty_Forward
+ and Diagnostics.warn_on_parse_using_empty_Forward
+ not in self.suppress_warnings_
+ ):
# walk stack until parse_string, scan_string, search_string, or transform_string is found
parse_fns = [
"parse_string",
return ret
def _setResultsName(self, name, list_all_matches=False):
- if __diag__.warn_name_set_on_empty_Forward:
+ if (
+ __diag__.warn_name_set_on_empty_Forward
+ and Diagnostics.warn_name_set_on_empty_Forward
+ not in self.suppress_warnings_
+ ):
if self.expr is None:
warnings.warn(
"{}: setting results name {!r} on {} expression "
# global helpers
#
def delimited_list(
- expr: ParserElement,
+ expr: Union[str, ParserElement],
delim: Union[str, ParserElement] = ",",
combine: bool = False,
*,
delimited_list(Word(alphas)).parse_string("aa,bb,cc") # -> ['aa', 'bb', 'cc']
delimited_list(Word(hexnums), delim=':', combine=True).parse_string("AA:BB:CC:DD:EE") # -> ['AA:BB:CC:DD:EE']
"""
+ if isinstance(expr, str_type):
+ expr = ParserElement._literalStringClass(expr)
+
dlName = "{expr} [{delim} {expr}]...{end}".format(
expr=str(expr.streamline()),
delim=str(delim),
locMarker = Empty().set_parse_action(lambda s, loc, t: loc)
endlocMarker = locMarker.copy()
endlocMarker.callPreparse = False
- # prefix these transient names with _NOWARN to suppress ungrouped name warnings
- matchExpr = (
- locMarker("_NOWARN_original_start")
- + expr
- + endlocMarker("_NOWARN_original_end")
- )
+ matchExpr = locMarker("_original_start") + expr + endlocMarker("_original_end")
if asString:
- extractText = lambda s, l, t: s[
- t._NOWARN_original_start : t._NOWARN_original_end
- ]
+ extractText = lambda s, l, t: s[t._original_start : t._original_end]
else:
def extractText(s, l, t):
- t[:] = [s[t.pop("_NOWARN_original_start") : t.pop("_NOWARN_original_end")]]
+ t[:] = [s[t.pop("_original_start") : t.pop("_original_end")]]
matchExpr.set_parse_action(extractText)
matchExpr.ignoreExprs = expr.ignoreExprs
+ matchExpr.suppress_warning(Diagnostics.warn_ungrouped_named_tokens_in_collection)
return matchExpr
return ar
@contextlib.contextmanager
- def assertDoesNotWarn(self, msg: str = None):
- if msg is None:
- msg = "unexpected warning raised"
+ 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:
- self.fail("{}: {}".format(msg, e))
+ if msg is None:
+ msg = "unexpected warning {} raised".format(e)
+ if isinstance(e, warning_type):
+ self.fail("{}: {}".format(msg, e))
+ else:
+ raise
class Test01_PyparsingTestInit(TestCase):
def runTest(self):
- from pyparsing import (
- __version__ as pyparsing_version,
- __version_time__ as pyparsing_version_time,
- )
-
print(
"Beginning test of pyparsing, version",
- pyparsing_version,
- pyparsing_version_time,
+ pp.__version__,
+ pp.__version_time__,
)
print("Python version", sys.version)
+ print("__version_info__ :", pp.__version_info__)
+ print("__version_info__ repr:", repr(pp.__version_info__))
class Test01a_PyparsingEnvironmentTests(TestCase):
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.parseString("x"))
+ except ParseException as pe:
+ pass
+
+
class Test02_WithoutPackrat(ppt.TestParseResultsAsserts, TestCase):
suite_context = None
save_suite_context = None
msg="failure in transformString",
)
+ def testTransformStringWithLeadingWhitespace(self):
+ sample = "\n\ncheck"
+ sample = " check"
+ keywords = pp.oneOf("aaa bbb", asKeyword=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.upcaseTokens)
+ transformed = ident.transformString(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.searchString(sample))
+
+ def testTransformStringWithExpectedLeadingWhitespace(self):
+ sample1 = "\n\ncheck aaa"
+ sample2 = " check aaa"
+ keywords = pp.oneOf("aaa bbb", asKeyword=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.upcaseTokens)
+
+ for sample in sample1, sample2:
+ transformed = (keywords | ident).transformString(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).transformString(test_string)
+ print(repr(result))
+ self.assertEqual(
+ test_string,
+ result,
+ "Failed whitespace skipping with NotAny and MatchFirst/Or",
+ )
+
def testUpdateDefaultWhitespace(self):
prev_default_whitespace_chars = pp.ParserElement.DEFAULT_WHITE_CHARS
try:
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()"""
):
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.runTests(
"""
not the bird
):
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")
+
expr.runTests(
"""\
not the bird
):
path = coord[...].setResultsName("path")
- def testDontWarnUngroupedNamedTokensIfUngroupedNamesStartWithNOWARN(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(
+ 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).setResultsName("path")
+
+ def testDontWarnUngroupedNamedTokensIfWarningSuppressed(self):
with ppt.reset_pyparsing_context():
pp.enable_diag(pp.Diagnostics.warn_ungrouped_named_tokens_in_collection)
with self.assertDoesNotWarn(
- msg="raised {} warning inner names start with '_NOWARN'".format(
+ msg="raised {} warning when warn on ungrouped named tokens was suppressed (original_text_for)".format(
pp.Diagnostics.warn_ungrouped_named_tokens_in_collection
)
):
):
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
with self.assertWarns(
UserWarning,
- msg="failed to warn when naming an empty Forward expression",
+ msg="failed to warn when parsing using an empty Forward expression",
+ ):
+ try:
+ print(base.parseString("x"))
+ 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.parseString("x"))
except ParseException as pe:
):
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 oneOf is
print(bool_list2)
self.assertEqual("bool [, bool]...", str(bool_list2))
+ 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 testEnableDebugOnNamedExpressions(self):
"""
- enable_debug_on_named_expressions - flag to auto-enable debug on all subsequent
[tox]
+skip_missing_interpreters=true
envlist =
- py{36,37,38,39,310,py3}
+ py{36,37,38,39,310,py3},pyparsing_packaging
[testenv]
deps=coverage
commands=
coverage run --parallel --branch -m unittest
+[testenv:pyparsing_packaging]
+deps=
+ pretend
+ pytest
+commands=
+ python -c "import shutil,os,stat;os.path.exists('packaging') and shutil.rmtree('packaging', onerror=lambda fn, path, _:os.chmod(path,stat.S_IWRITE) or fn(path))"
+ git clone --depth 1 https://github.com/pypa/packaging.git
+ python -m pytest packaging/tests
+ python -c "import shutil,os,stat;shutil.rmtree('packaging', onerror=lambda fn, path, _:os.chmod(path,stat.S_IWRITE) or fn(path))"