From d62e8248057d10de9e2ecbdbe882352e2a5db4f3 Mon Sep 17 00:00:00 2001 From: David Hagen Date: Sat, 28 Apr 2018 07:06:54 -0400 Subject: [PATCH] Remove duplicate expected values in failure message Previosuly, if multiple parsing branches expected the same token, this token would appear multiple times in the list of expected values. Now, this list is uniquified. --- parsita/options.py | 18 +++++------------- parsita/parsers.py | 34 ++++++++++++++++++++++++++++++++-- tests/test_basic.py | 9 +++++++++ 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/parsita/options.py b/parsita/options.py index 7ce92f7..65ac9ff 100644 --- a/parsita/options.py +++ b/parsita/options.py @@ -1,7 +1,7 @@ import re from typing import Any, Sequence -from .state import Input, Output, SequenceReader, StringReader, Result, Success, Failure, Continue +from .state import Input, Output, SequenceReader, StringReader, Result # Global mutable state @@ -24,27 +24,19 @@ def wrap_literal(literal: Input): def default_parse_method(self, source: str) -> Result[Output]: - from .parsers import eof + from .parsers import completely_parse_reader reader = StringReader(source) - result = (self << eof).consume(reader) - if isinstance(result, Continue): - return Success(result.value) - else: - return Failure(result.farthest.expected_error(' or '.join(map(lambda x: x(), result.expected)))) + return completely_parse_reader(self, reader) def basic_parse(self, source: Sequence[Input]) -> Result[Output]: - from .parsers import eof + from .parsers import completely_parse_reader reader = SequenceReader(source) - result = (self << eof).consume(reader) - if isinstance(result, Continue): - return Success(result.value) - else: - return Failure(result.farthest.expected_error(' or '.join(map(lambda x: x(), result.expected)))) + return completely_parse_reader(self, reader) parse_method = default_parse_method diff --git a/parsita/parsers.py b/parsita/parsers.py index e3f3630..c25c6b8 100644 --- a/parsita/parsers.py +++ b/parsita/parsers.py @@ -3,7 +3,7 @@ from types import MethodType from . import options -from .state import Input, Output, Convert, Reader, StringReader, Result, Status, Continue, Backtrack +from .state import Input, Output, Convert, Reader, StringReader, Result, Status, Success, Failure, Continue, Backtrack class Parser(Generic[Input, Output]): @@ -152,6 +152,36 @@ def __gt__(self, other) -> 'ConversionParser': return ConversionParser(self, other) +def completely_parse_reader(parser: Parser[Input, Output], reader: Reader[Input]) -> Result[Output]: + """Consume reader and return Success only on complete consumption. + + This is a helper function for ``parse`` methods, which return ``Success`` + when the input is completely consumed and ``Failure`` with an appropriate + message otherwise. + + Args: + parser: The parser doing the consuming + reader: The input being consumed + + Returns: + A parsing ``Result`` + """ + result = (parser << eof).consume(reader) + + if isinstance(result, Continue): + return Success(result.value) + else: + used = set() + unique_expected = [] + for expected_lambda in result.expected: + expected = expected_lambda() + if expected not in used: + used.add(expected) + unique_expected.append(expected) + + return Failure(result.farthest.expected_error(' or '.join(unique_expected))) + + class LiteralParser(Generic[Input], Parser[Input, Input]): def __init__(self, pattern: Sequence[Input]): super().__init__() @@ -677,4 +707,4 @@ def failure(expected: str = ''): 'AlternativeParser', 'SequentialParser', 'DiscardLeftParser', 'DiscardRightParser', 'RepeatedOnceParser', 'rep1', 'RepeatedParser', 'rep', 'RepeatedOnceSeparatedParser', 'rep1sep', 'RepeatedSeparatedParser', 'repsep', 'ConversionParser', 'EndOfSourceParser', 'eof', 'SuccessParser', 'success', 'FailureParser', - 'failure'] + 'failure', 'completely_parse_reader'] diff --git a/tests/test_basic.py b/tests/test_basic.py index 9109f35..7827fb0 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -160,6 +160,15 @@ class TestParsers(GeneralParsers): self.assertEqual(TestParsers.ab.parse('a'), Success('a')) + def test_multiple_messages_duplicate(self): + class TestParsers(GeneralParsers): + a = lit('a') + ab = a & 'b' + ac = a & 'c' + either = ab | ac + + self.assertEqual(TestParsers.either.parse('cc'), Failure('Expected a but found c at index 0')) + class SequentialTestCase(TestCase): def test_sequential(self):