From d5c9fa47919f64df0b3f9db6a062b39ecdaa3c93 Mon Sep 17 00:00:00 2001 From: Igor Dejanovic <igor.dejanovic@gmail.com> Date: Tue, 5 Aug 2014 01:01:45 +0200 Subject: [PATCH] Parse tree navigation using dot syntax + unit tests. --- arpeggio/__init__.py | 55 +++++++++++----- .../test_ptnode_navigation_expressions.py | 63 +++++++++++++++++++ tests/unit/test_rulename_lookup.py | 30 --------- 3 files changed, 102 insertions(+), 46 deletions(-) create mode 100644 tests/unit/test_ptnode_navigation_expressions.py delete mode 100644 tests/unit/test_rulename_lookup.py diff --git a/arpeggio/__init__.py b/arpeggio/__init__.py index cdbcca7..495cbce 100644 --- a/arpeggio/__init__.py +++ b/arpeggio/__init__.py @@ -677,50 +677,73 @@ class Terminal(ParseTreeNode): class NonTerminal(ParseTreeNode, list): """ Non-leaf node of the Parse Tree. Represents language syntax construction. + At the same time used in ParseTreeNode navigation expressions. + See test_ptnode_navigation_expressions.py for examples of navigation expressions. Attributes: nodes (list of ParseTreeNode): Children parse tree nodes. + _filtered (bool): Is this NT a dynamically created filtered NT. + This is used internally. """ - def __init__(self, rule, position, nodes, error=False): + def __init__(self, rule, position, nodes, error=False, _filtered=False): super(NonTerminal, self).__init__(rule, position, error) self.extend(flatten([nodes])) + self._filtered = _filtered - # Child nodes cache. Used for lookup by rule name. - self._child_cache = {} + # Navigation expression cache. Used for lookup by rule name. + self._expr_cache = {} + + @property + def value(self): + """Terminal protocol.""" + return str(self) @property def desc(self): return self.name - # def __iter__(self): - # return self - def __str__(self): return " | ".join([str(x) for x in self]) def __repr__(self): return "[ %s ]" % ", ".join([repr(x) for x in self]) - def __getattr__(self, item): + def __getattr__(self, rule_name): """ Find a child (non)terminal by the rule name. Args: - item(str): The name of the child node. + rule_name(str): The name of the rule that is referenced from + this node rule. """ + # Prevent infinite recursion + if rule_name == '_expr_cache': + raise AttributeError + # First check the cache - if item in self._child_cache: - return self._child_cache[item] + if rule_name in self._expr_cache: + return self._expr_cache[rule_name] - # If not found in the cache find it and store it in the - # cache for later. + # If result is not found in the cache collect all nodes + # with the given rule name and create new NonTerminal + # and cache it for later access. + nodes = [] for n in self: - if n.rule == item: - self._child_cache[item] = n - return n + if self._filtered: + # For filtered NT rule_name is a rule on + # each of its children + for m in n: + if m.rule == rule_name: + nodes.append(m) + else: + if n.rule == rule_name: + nodes.append(n) - raise AttributeError + # For expression NonTerminals instances position does not have any sense. + result = NonTerminal(rule=rule_name, position=None, nodes=nodes, _filtered=True) + self._expr_cache[rule_name] = result + return result # ---------------------------------------------------- diff --git a/tests/unit/test_ptnode_navigation_expressions.py b/tests/unit/test_ptnode_navigation_expressions.py new file mode 100644 index 0000000..53cf7d4 --- /dev/null +++ b/tests/unit/test_ptnode_navigation_expressions.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +####################################################################### +# Name: test_ptnode_navigation_expressions +# Purpose: Test ParseTreeNode navigation expressions. +# Author: Igor R. Dejanović <igor DOT dejanovic AT gmail DOT com> +# Copyright: (c) 2014 Igor R. Dejanović <igor DOT dejanovic AT gmail DOT com> +# License: MIT License +####################################################################### + +import pytest + +# Grammar +from arpeggio import ParserPython, ZeroOrMore, ParseTreeNode, NonTerminal +from arpeggio.export import PTDOTExporter + + +def foo(): return "a", bar, "b", baz, bar2, ZeroOrMore(bar) +def bar(): return [bla, bum], baz, "c" +def bar2():return ZeroOrMore(bla) +def baz(): return "d" +def bla(): return "bla" +def bum(): return ["bum", "bam"] + + +def test_lookup_single(): + + parser = ParserPython(foo, reduce_tree=False) + + result = parser.parse("a bum d c b d bla bum d c") + + # Uncomment following line to visualize the parse tree in graphviz + # PTDOTExporter().exportFile(result, 'test_ptnode_navigation_expressions.dot') + + assert isinstance(result, ParseTreeNode) + assert isinstance(result.bar, NonTerminal) + # dot access + assert result.bar.rule == 'bar' + # Index access + assert result[1].rule == 'bar' + + # There are six children from result + assert len(result) == 6 + + # There is two bar matched from result (at the begging and from ZeroOrMore) + # Dot access collect all NTs from the given path + assert len(result.bar) == 2 + # Verify position + assert result.bar[0].position == 2 + assert result.bar[1].position == 18 + + # Multilevel dot access returns all elements from all previous ones. + # For example this returns all bum from all bar in result + assert len(result.bar.bum) == 2 + # Verify that proper bum are returned + assert result.bar.bum[0].rule == 'bum' + assert result.bar.bum[1].position == 18 + + # The same for all bla from all bar2 + assert len(result.bar2.bla) == 1 + + assert hasattr(result, "bar") + assert hasattr(result, "baz") + diff --git a/tests/unit/test_rulename_lookup.py b/tests/unit/test_rulename_lookup.py deleted file mode 100644 index 4c733bf..0000000 --- a/tests/unit/test_rulename_lookup.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -####################################################################### -# Name: test_peg_parser -# Purpose: Test for parser constructed using PEG textual grammars. -# Author: Igor R. Dejanović <igor DOT dejanovic AT gmail DOT com> -# Copyright: (c) 2014 Igor R. Dejanović <igor DOT dejanovic AT gmail DOT com> -# License: MIT License -####################################################################### - -import pytest - -# Grammar -from arpeggio import ParserPython, ZeroOrMore - - -def foo(): return "a", bar, "b", baz -def bar(): return "c" -def baz(): return "d" - - -def test_lookup_single(): - - parser = ParserPython(foo) - - result = parser.parse("a c b d") - - assert hasattr(result, "bar") - assert hasattr(result, "baz") - assert not hasattr(result, "unexisting") - -- 2.18.0