diff --git a/docs/recipes.md b/docs/recipes.md index 6a35591..2d2f83a 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -145,3 +145,30 @@ class Parent(Visitor): assert not hasattr(subtree, 'parent') subtree.parent = tree ``` + + +## Unwinding VisitError after a transformer/visitor exception + +Errors that happen inside visitors and transformers get wrapped inside a `VisitError` exception. + +This can often be inconvenient, if you wish the actual error to propagate upwards, or if you want to catch it. + +But, it's easy to unwrap it at the point of calling the transformer, by catching it and raising the `VisitError.orig_exc` attribute. + +For example: +```python +from lark import Lark, Transformer +from lark.visitors import VisitError + +tree = Lark('start: "a"').parse('a') + +class T(Transformer): + def start(self, x): + raise KeyError("Original Exception") + +t = T() +try: + print( t.transform(tree)) +except VisitError as e: + raise e.orig_exc +``` \ No newline at end of file diff --git a/lark/exceptions.py b/lark/exceptions.py index d606792..26ffce3 100644 --- a/lark/exceptions.py +++ b/lark/exceptions.py @@ -148,7 +148,7 @@ class UnexpectedEOF(ParseError, UnexpectedInput): class UnexpectedCharacters(LexError, UnexpectedInput): def __init__(self, seq, lex_pos, line, column, allowed=None, considered_tokens=None, state=None, token_history=None, - terminals_by_name=None): + terminals_by_name=None, considered_rules=None): # TODO considered_tokens and allowed can be figured out using state self.line = line self.column = column @@ -158,6 +158,7 @@ class UnexpectedCharacters(LexError, UnexpectedInput): self.allowed = allowed self.considered_tokens = considered_tokens + self.considered_rules = considered_rules self.token_history = token_history if isinstance(seq, bytes): @@ -169,7 +170,7 @@ class UnexpectedCharacters(LexError, UnexpectedInput): super(UnexpectedCharacters, self).__init__() def __str__(self): - message = "No terminal defined for '%s' at line %d col %d" % (self.char, self.line, self.column) + message = "No terminal matches '%s' in the current parser context, at line %d col %d" % (self.char, self.line, self.column) message += '\n\n' + self._context if self.allowed: message += self._format_expected(self.allowed) diff --git a/lark/parsers/xearley.py b/lark/parsers/xearley.py index 79ac82f..335d9ad 100644 --- a/lark/parsers/xearley.py +++ b/lark/parsers/xearley.py @@ -114,8 +114,11 @@ class Parser(BaseParser): del delayed_matches[i+1] # No longer needed, so unburden memory if not next_set and not delayed_matches and not next_to_scan: + considered_rules = list(sorted(to_scan, key=lambda key: key.rule.origin.name)) raise UnexpectedCharacters(stream, i, text_line, text_column, {item.expect.name for item in to_scan}, - set(to_scan), state=frozenset(i.s for i in to_scan)) + set(to_scan), state=frozenset(i.s for i in to_scan), + considered_rules=considered_rules + ) return next_to_scan