From 3cbc4a738186daceba417d79bd77035bf8f69060 Mon Sep 17 00:00:00 2001 From: Nicholas Chammas Date: Sun, 13 Jul 2025 21:25:32 -0400 Subject: [PATCH 1/4] tweak docs --- docs/visitors.rst | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/visitors.rst b/docs/visitors.rst index 323c18375..10bfdd0d0 100644 --- a/docs/visitors.rst +++ b/docs/visitors.rst @@ -4,7 +4,7 @@ Transformers & Visitors Transformers & Visitors provide a convenient interface to process the parse-trees that Lark returns. -They are used by inheriting from the correct class (visitor or transformer), +They are used by inheriting from one of the classes described here, and implementing methods corresponding to the rule you wish to process. Each method accepts the children as an argument. That can be modified using the ``v_args`` decorator, which allows one to inline the arguments (akin to ``*args``), @@ -17,9 +17,8 @@ See: `visitors.py`_ Visitor ------- -Visitors visit each node of the tree, and run the appropriate method on it according to the node's data. - -They work bottom-up, starting with the leaves and ending at the root of the tree. +Visitors visit each node of the tree and run the appropriate method on it according to the node's data. +They can work top-down or bottom-up, depending on what method you use. There are two classes that implement the visitor interface: @@ -45,12 +44,12 @@ Example: Interpreter ----------- -.. autoclass:: lark.visitors.Interpreter - - Example: :: + from lark.visitors import Interpreter + + class IncreaseSomeOfTheNumbers(Interpreter): def number(self, tree): tree.children[0] += 1 @@ -59,7 +58,12 @@ Example: # skip this subtree. don't change any number node inside it. pass - IncreaseSomeOfTheNumbers().visit(parse_tree) + + IncreaseSomeOfTheNumbers().visit(parse_tree) + + +.. autoclass:: lark.visitors.Interpreter + :members: visit, visit_children, __default__ Transformer ----------- @@ -98,14 +102,11 @@ Example: .. autoclass:: lark.visitors.Transformer_InPlaceRecursive -v_args ------- +Useful Utilities +---------------- .. autofunction:: lark.visitors.v_args - -merge_transformers ------------------- - +.. autofunction:: lark.visitors.visit_children_decor .. autofunction:: lark.visitors.merge_transformers Discard From a383d4d6cc30626af508c1b41fa7531354a25159 Mon Sep 17 00:00:00 2001 From: Nicholas Chammas Date: Sun, 13 Jul 2025 21:26:00 -0400 Subject: [PATCH 2/4] add missing docstrings, have decorators error when used incorrectly --- lark/visitors.py | 53 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/lark/visitors.py b/lark/visitors.py index 47f014647..15b317510 100644 --- a/lark/visitors.py +++ b/lark/visitors.py @@ -417,11 +417,16 @@ class Interpreter(_Decoratable, ABC, Generic[_Leaf_T, _Return_T]): """ def visit(self, tree: Tree[_Leaf_T]) -> _Return_T: + "Visit the tree, starting with the root and finally the leaves (top-down)." # There are no guarantees on the type of the value produced by calling a user func for a # child will produce. So only annotate the public method and use an internal method when # visiting child trees. return self._visit_tree(tree) + def visit_topdown(self, tree: Tree[_Leaf_T]): + "Interpreters only work top-down. This method is identical to `visit()`." + return self.visit(tree) + def _visit_tree(self, tree: Tree[_Leaf_T]): f = getattr(self, tree.data) wrapper = getattr(f, 'visit_wrapper', None) @@ -431,22 +436,45 @@ def _visit_tree(self, tree: Tree[_Leaf_T]): return f(tree) def visit_children(self, tree: Tree[_Leaf_T]) -> List: - return [self._visit_tree(child) if isinstance(child, Tree) else child - for child in tree.children] + "Visit all the children of this tree and return the results as a list." + return [ + self._visit_tree(child) + if isinstance(child, Tree) + else child + for child in tree.children + ] def __getattr__(self, name): return self.__default__ def __default__(self, tree): + """ + Default function that is called if there is no attribute matching ``tree.data``. + + Can be overridden. Defaults to visiting all the tree's children. + """ return self.visit_children(tree) _InterMethod = Callable[[Type[Interpreter], _Return_T], _R] def visit_children_decor(func: _InterMethod) -> _InterMethod: - "See Interpreter" + """ + A wrapper around Interpreter methods. It makes the wrapped node method automatically visit the + node's children before proceeding with the logic you have defined for that node. + + Example: + :: + + class ProcessQuery(Interpreter): + @visit_children_decor + def query(self, tree): + pass + """ @wraps(func) def inner(cls, tree): + if not isinstance(cls, Interpreter): + raise TypeError("visit_children_decor can only be applied to Interpreter methods.") values = cls.visit_children(tree) return func(cls, values) return inner @@ -511,11 +539,12 @@ def _vargs_tree(f, data, children, meta): def v_args(inline: bool = False, meta: bool = False, tree: bool = False, wrapper: Optional[Callable] = None) -> Callable[[_DECORATED], _DECORATED]: - """A convenience decorator factory for modifying the behavior of user-supplied callback methods of ``Transformer`` classes. + """A convenience decorator factory for modifying the behavior of user-supplied callback methods + of ``Transformer`` or ``Interpreter`` classes. - By default, transformer callback methods accept one argument - a list of the node's children. + By default, the callback methods for these classes accept one argument - a list of the node's children. - ``v_args`` can modify this behavior. When used on a ``Transformer`` class definition, it applies to + ``v_args`` can modify this behavior. When used on the class definition, it applies to all the callback methods inside it. ``v_args`` can be applied to a single method, or to an entire class. When applied to both, @@ -567,6 +596,18 @@ def tree_node(self, tree): func = wrapper def _visitor_args_dec(obj): + from inspect import isclass + if isclass(obj) and issubclass(obj, Visitor): + raise TypeError("v_args cannot be applied to Visitor classes.") + if callable(obj) and not isclass(obj): + @wraps(obj) + def method_wrapper(*args, **kwargs): + if args: + self_instance = args[0] + if isinstance(self_instance, Visitor): + raise TypeError("v_args cannot be applied to Visitor methods.") + return obj(*args, **kwargs) + return _apply_v_args(method_wrapper, func) return _apply_v_args(obj, func) return _visitor_args_dec From 411c2f3615cbd6187f1fc4977998678cfb9cff08 Mon Sep 17 00:00:00 2001 From: Nicholas Chammas Date: Sun, 13 Jul 2025 21:26:14 -0400 Subject: [PATCH 3/4] add tests for changed behavior --- tests/test_trees.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_trees.py b/tests/test_trees.py index 82eceab19..77d963861 100644 --- a/tests/test_trees.py +++ b/tests/test_trees.py @@ -131,6 +131,10 @@ def c(self, tree): return 'C' self.assertEqual(Interp3().visit(t), list('BCd')) + self.assertEqual( + Interp3().visit(t), + Interp3().visit_topdown(t), + ) def test_transformer(self): t = Tree('add', [Tree('sub', [Tree('i', ['3']), Tree('f', ['1.1'])]), Tree('i', ['1'])]) @@ -471,6 +475,30 @@ def INT(self, value): assert MyTransformer().transform(t) is None + def test_incorrect_use_of_decorators(self): + class TestVisitor1(Visitor): + @visit_children_decor + def a(self, tree): + pass + + @v_args(inline=True) + def b(self, tree): + pass + + with self.assertRaises(TypeError) as e: + TestVisitor1().visit_topdown(self.tree1) + self.assertIn("visit_children_decor", str(e.exception)) + self.assertIn("Interpreter", str(e.exception)) + with self.assertRaises(TypeError) as e: + TestVisitor1().visit(self.tree1) + self.assertIn("v_args", str(e.exception)) + + with self.assertRaises(TypeError) as e: + @v_args(inline=True) + class TestVisitor2(Visitor): + def a(self, tree): + pass + self.assertIn("v_args", str(e.exception)) if __name__ == '__main__': unittest.main() From ab6ddff26f2416642217f1cb593477b87ef3e059 Mon Sep 17 00:00:00 2001 From: Nicholas Chammas Date: Mon, 14 Jul 2025 13:33:30 -0400 Subject: [PATCH 4/4] remove implementation of visit_topdown --- lark/visitors.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lark/visitors.py b/lark/visitors.py index 15b317510..81d796e56 100644 --- a/lark/visitors.py +++ b/lark/visitors.py @@ -423,10 +423,6 @@ def visit(self, tree: Tree[_Leaf_T]) -> _Return_T: # visiting child trees. return self._visit_tree(tree) - def visit_topdown(self, tree: Tree[_Leaf_T]): - "Interpreters only work top-down. This method is identical to `visit()`." - return self.visit(tree) - def _visit_tree(self, tree: Tree[_Leaf_T]): f = getattr(self, tree.data) wrapper = getattr(f, 'visit_wrapper', None)