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