Repository: erikrose/parsimonious

Issue:
Unnamed nodes should make up useful names for themselves
When you print a node, you get something like this:

```
<Node called "char_range" matching "c-a">
    <Node called "class_char" matching "c">
        <RegexNode matching "c">
    <Node matching "-">
    <Node called "class_char" matching "a">
        <RegexNode matching "a">
```

Named nodes get names, but unnamed ones (like the one matching "-") don't. Especially when debugging—for instance, ending up in visit_generic and not knowing what node triggered that dispatch—it would be helpful if nodes would identify themselves by their rule expression: `<Node "class_item*" matching "abc">`. Then we take the "called" out of the named nodes, and everything is frighteningly consistent.


Discussion:

Patch:
diff --git a/parsimonious/expressions.py b/parsimonious/expressions.py
index 5e2d2b9..dda8fd9 100644
--- a/parsimonious/expressions.py
+++ b/parsimonious/expressions.py
@@ -78,7 +78,7 @@ def expression(callable, rule_name, grammar):
             else:
                 # Node or None
                 return result
-            return Node(self.name, text, pos, end, children=children)
+            return Node(self, text, pos, end, children=children)
 
         def _as_rhs(self):
             return '{custom function "%s"}' % callable.__name__
@@ -94,10 +94,20 @@ class Expression(StrAndRepr):
     # http://stackoverflow.com/questions/1336791/dictionary-vs-object-which-is-more-efficient-and-why
 
     # Top-level expressions--rules--have names. Subexpressions are named ''.
-    __slots__ = ['name']
+    __slots__ = ['name', 'identity_tuple']
 
     def __init__(self, name=''):
         self.name = name
+        self.identity_tuple = (self.name, )
+
+    def __hash__(self):
+        return hash(self.identity_tuple)
+
+    def __eq__(self, other):
+        return isinstance(other, self.__class__) and self.identity_tuple == other.identity_tuple
+
+    def __ne__(self, other):
+        return not (self == other)
 
     def parse(self, text, pos=0):
         """Return a parse tree of ``text``.
@@ -180,10 +190,9 @@ class Expression(StrAndRepr):
         return node
 
     def __str__(self):
-        return u'<%s %s at 0x%s>' % (
+        return u'<%s %s>' % (
             self.__class__.__name__,
-            self.as_rule(),
-            id(self))
+            self.as_rule())
 
     def as_rule(self):
         """Return the left- and right-hand sides of a rule that represents me.
@@ -223,10 +232,11 @@ class Literal(Expression):
     def __init__(self, literal, name=''):
         super(Literal, self).__init__(name)
         self.literal = literal
+        self.identity_tuple = (name, literal)
 
     def _uncached_match(self, text, pos, cache, error):
         if text.startswith(self.literal, pos):
-            return Node(self.name, text, pos, pos + len(self.literal))
+            return Node(self, text, pos, pos + len(self.literal))
 
     def _as_rhs(self):
         # TODO: Get backslash escaping right.
@@ -241,7 +251,7 @@ class TokenMatcher(Literal):
     """
     def _uncached_match(self, token_list, pos, cache, error):
         if token_list[pos].type == self.literal:
-            return Node(self.name, token_list, pos, pos + 1)
+            return Node(self, token_list, pos, pos + 1)
 
 
 class Regex(Expression):
@@ -262,13 +272,14 @@ class Regex(Expression):
                                       (dot_all and re.S) |
                                       (unicode and re.U) |
                                       (verbose and re.X))
+        self.identity_tuple = (self.name, self.re)
 
     def _uncached_match(self, text, pos, cache, error):
         """Return length of match, ``None`` if no match."""
         m = self.re.match(text, pos)
         if m is not None:
             span = m.span()
-            node = RegexNode(self.name, text, pos, pos + span[1] - span[0])
+            node = RegexNode(self, text, pos, pos + span[1] - span[0])
             node.match = m  # TODO: A terrible idea for cache size?
             return node
 
@@ -293,6 +304,18 @@ class Compound(Expression):
         super(Compound, self).__init__(kwargs.get('name', ''))
         self.members = members
 
+    def __hash__(self):
+        # Note we leave members out of the hash computation, since compounds can get added to
+        # sets, then have their members mutated. See RuleVisitor._resolve_refs.
+        # Equality should still work, but we want the rules to go into the correct hash bucket.
+        return hash((self.__class__, self.name))
+
+    def __eq__(self, other):
+        return (
+            isinstance(other, self.__class__) and
+            self.name == other.name and
+            self.members == other.members)
+
 
 class Sequence(Compound):
     """A series of expressions that must match contiguous, ordered pieces of
@@ -315,7 +338,7 @@ class Sequence(Compound):
             new_pos += length
             length_of_sequence += length
         # Hooray! We got through all the members!
-        return Node(self.name, text, pos, pos + length_of_sequence, children)
+        return Node(self, text, pos, pos + length_of_sequence, children)
 
     def _as_rhs(self):
         return u'({0})'.format(u' '.join(self._unicode_members()))
@@ -333,7 +356,7 @@ class OneOf(Compound):
             node = m.match_core(text, pos, cache, error)
             if node is not None:
                 # Wrap the succeeding child in a node representing the OneOf:
-                return Node(self.name, text, pos, node.end, children=[node])
+                return Node(self, text, pos, node.end, children=[node])
 
     def _as_rhs(self):
         return u'({0})'.format(u' / '.join(self._unicode_members()))
@@ -350,7 +373,7 @@ class Lookahead(Compound):
     def _uncached_match(self, text, pos, cache, error):
         node = self.members[0].match_core(text, pos, cache, error)
         if node is not None:
-            return Node(self.name, text, pos, pos)
+            return Node(self, text, pos, pos)
 
     def _as_rhs(self):
         return u'&%s' % self._unicode_members()[0]
@@ -367,7 +390,7 @@ class Not(Compound):
         # not bother to cache NOTs directly.
         node = self.members[0].match_core(text, pos, cache, error)
         if node is None:
-            return Node(self.name, text, pos, pos)
+            return Node(self, text, pos, pos)
 
     def _as_rhs(self):
         # TODO: Make sure this parenthesizes the member properly if it's an OR
@@ -386,8 +409,8 @@ class Optional(Compound):
     """
     def _uncached_match(self, text, pos, cache, error):
         node = self.members[0].match_core(text, pos, cache, error)
-        return (Node(self.name, text, pos, pos) if node is None else
-                Node(self.name, text, pos, node.end, children=[node]))
+        return (Node(self, text, pos, pos) if node is None else
+                Node(self, text, pos, node.end, children=[node]))
 
     def _as_rhs(self):
         return u'%s?' % self._unicode_members()[0]
@@ -404,7 +427,7 @@ class ZeroOrMore(Compound):
             node = self.members[0].match_core(text, new_pos, cache, error)
             if node is None or not (node.end - node.start):
                 # Node was None or 0 length. 0 would otherwise loop infinitely.
-                return Node(self.name, text, pos, new_pos, children)
+                return Node(self, text, pos, new_pos, children)
             children.append(node)
             new_pos += node.end - node.start
 
@@ -441,7 +464,7 @@ class OneOrMore(Compound):
                 break
             new_pos += length
         if len(children) >= self.min:
-            return Node(self.name, text, pos, new_pos, children)
+            return Node(self, text, pos, new_pos, children)
 
     def _as_rhs(self):
         return u'%s+' % self._unicode_members()[0]
diff --git a/parsimonious/grammar.py b/parsimonious/grammar.py
index b6386d0..b7b1c29 100644
--- a/parsimonious/grammar.py
+++ b/parsimonious/grammar.py
@@ -420,8 +420,8 @@ class RuleVisitor(NodeVisitor):
                 # of `expr.members` can refer back to `expr`, but it can't go
                 # any farther.
                 done.add(expr)
-                expr.members = [self._resolve_refs(rule_map, member, done)
-                                for member in expr.members]
+                expr.members = tuple(self._resolve_refs(rule_map, member, done)
+                                     for member in expr.members)
             return expr
 
     def visit_rules(self, node, rules_list):
diff --git a/parsimonious/nodes.py b/parsimonious/nodes.py
index 77ad1f9..8c8d1af 100644
--- a/parsimonious/nodes.py
+++ b/parsimonious/nodes.py
@@ -34,20 +34,25 @@ class Node(object):
     """
     # I tried making this subclass list, but it got ugly. I had to construct
     # invalid ones and patch them up later, and there were other problems.
-    __slots__ = ['expr_name',  # The name of the expression that generated me
+    __slots__ = ['expr',  # The expression that generated me
                  'full_text',  # The full text fed to the parser
                  'start', # The position in the text where that expr started matching
                  'end',   # The position after start where the expr first didn't
                           # match. [start:end] follow Python slice conventions.
                  'children']  # List of child parse tree nodes
 
-    def __init__(self, expr_name, full_text, start, end, children=None):
-        self.expr_name = expr_name
+    def __init__(self, expr, full_text, start, end, children=None):
+        self.expr = expr
         self.full_text = full_text
         self.start = start
         self.end = end
         self.children = children or []
 
+    @property
+    def expr_name(self):
+        # backwards compatibility
+        return self.expr.name
+
     def __iter__(self):
         """Support looping over my children and doing tuple unpacks on me.
 
@@ -92,7 +97,7 @@ class Node(object):
         if not isinstance(other, Node):
             return NotImplemented
 
-        return (self.expr_name == other.expr_name and
+        return (self.expr == other.expr and
                 self.full_text == other.full_text and
                 self.start == other.start and
                 self.end == other.end and
@@ -109,7 +114,7 @@ class Node(object):
         ret = ["s = %r" % self.full_text] if top_level else []
         ret.append("%s(%r, s, %s, %s%s)" % (
             self.__class__.__name__,
-            self.expr_name,
+            self.expr,
             self.start,
             self.end,
             (', children=[%s]' %
@@ -233,8 +238,8 @@ class NodeVisitor(with_metaclass(RuleDecoratorMeta, object)):
         for now.
 
         """
-        raise NotImplementedError('No visitor method was defined for "%s".' %
-                                  node.expr_name)
+        raise NotImplementedError('No visitor method was defined for this expression: %s' %
+                                  node.expr.as_rule())
 
     # Convenience methods:
 
diff --git a/tox.ini b/tox.ini
index f1fa5e6..9a8519a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,4 +11,6 @@ envlist = py27, py33, py34, py35, py36
 [testenv]
 usedevelop = True
 commands = nosetests parsimonious
-deps = nose
+deps =
+  nose
+  six>=1.9.0


Test patch:
diff --git a/parsimonious/tests/test_expressions.py b/parsimonious/tests/test_expressions.py
index 2d0360b..58734b9 100644
--- a/parsimonious/tests/test_expressions.py
+++ b/parsimonious/tests/test_expressions.py
@@ -82,49 +82,49 @@ class TreeTests(TestCase):
     def test_simple_node(self):
         """Test that leaf expressions like ``Literal`` make the right nodes."""
         h = Literal('hello', name='greeting')
-        eq_(h.match('hello'), Node('greeting', 'hello', 0, 5))
+        eq_(h.match('hello'), Node(h, 'hello', 0, 5))
 
     def test_sequence_nodes(self):
         """Assert that ``Sequence`` produces nodes with the right children."""
         s = Sequence(Literal('heigh', name='greeting1'),
                      Literal('ho',    name='greeting2'), name='dwarf')
         text = 'heighho'
-        eq_(s.match(text), Node('dwarf', text, 0, 7, children=
-                                [Node('greeting1', text, 0, 5),
-                                 Node('greeting2', text, 5, 7)]))
+        eq_(s.match(text), Node(s, text, 0, 7, children=
+                                [Node(s.members[0], text, 0, 5),
+                                 Node(s.members[1], text, 5, 7)]))
 
     def test_one_of(self):
         """``OneOf`` should return its own node, wrapping the child that succeeds."""
         o = OneOf(Literal('a', name='lit'), name='one_of')
         text = 'aa'
-        eq_(o.match(text), Node('one_of', text, 0, 1, children=[
-                                Node('lit', text, 0, 1)]))
+        eq_(o.match(text), Node(o, text, 0, 1, children=[
+                                Node(o.members[0], text, 0, 1)]))
 
     def test_optional(self):
         """``Optional`` should return its own node wrapping the succeeded child."""
         expr = Optional(Literal('a', name='lit'), name='opt')
 
         text = 'a'
-        eq_(expr.match(text), Node('opt', text, 0, 1, children=[
-                                   Node('lit', text, 0, 1)]))
+        eq_(expr.match(text), Node(expr, text, 0, 1, children=[
+                                   Node(expr.members[0], text, 0, 1)]))
 
         # Test failure of the Literal inside the Optional; the
         # LengthTests.test_optional is ambiguous for that.
         text = ''
-        eq_(expr.match(text), Node('opt', text, 0, 0))
+        eq_(expr.match(text), Node(expr, text, 0, 0))
 
     def test_zero_or_more_zero(self):
         """Test the 0 case of ``ZeroOrMore``; it should still return a node."""
         expr = ZeroOrMore(Literal('a'), name='zero')
         text = ''
-        eq_(expr.match(text), Node('zero', text, 0, 0))
+        eq_(expr.match(text), Node(expr, text, 0, 0))
 
     def test_one_or_more_one(self):
         """Test the 1 case of ``OneOrMore``; it should return a node with a child."""
         expr = OneOrMore(Literal('a', name='lit'), name='one')
         text = 'a'
-        eq_(expr.match(text), Node('one', text, 0, 1, children=[
-                                   Node('lit', text, 0, 1)]))
+        eq_(expr.match(text), Node(expr, text, 0, 1, children=[
+                                   Node(expr.members[0], text, 0, 1)]))
 
     # Things added since Grammar got implemented are covered in integration
     # tests in test_grammar.
@@ -142,9 +142,9 @@ class ParseTests(TestCase):
         """
         expr = OneOrMore(Literal('a', name='lit'), name='more')
         text = 'aa'
-        eq_(expr.parse(text), Node('more', text, 0, 2, children=[
-                                   Node('lit', text, 0, 1),
-                                   Node('lit', text, 1, 2)]))
+        eq_(expr.parse(text), Node(expr, text, 0, 2, children=[
+                                   Node(expr.members[0], text, 0, 1),
+                                   Node(expr.members[0], text, 1, 2)]))
 
 
 class ErrorReportingTests(TestCase):
diff --git a/parsimonious/tests/test_grammar.py b/parsimonious/tests/test_grammar.py
index 6051c3e..f9af0e8 100644
--- a/parsimonious/tests/test_grammar.py
+++ b/parsimonious/tests/test_grammar.py
@@ -9,7 +9,7 @@ from nose.tools import eq_, assert_raises, ok_
 from six import text_type
 
 from parsimonious.exceptions import UndefinedLabel, ParseError
-from parsimonious.expressions import Sequence
+from parsimonious.expressions import Literal, Lookahead, Regex, Sequence, TokenMatcher
 from parsimonious.grammar import rule_grammar, RuleVisitor, Grammar, TokenGrammar, LazyReference
 from parsimonious.nodes import Node
 from parsimonious.utils import Token
@@ -21,37 +21,40 @@ class BootstrappingGrammarTests(TestCase):
 
     def test_quantifier(self):
         text = '*'
-        eq_(rule_grammar['quantifier'].parse(text),
-            Node('quantifier', text, 0, 1, children=[
-                Node('', text, 0, 1), Node('_', text, 1, 1)]))
+        quantifier = rule_grammar['quantifier']
+        eq_(quantifier.parse(text),
+            Node(quantifier, text, 0, 1, children=[
+                Node(quantifier.members[0], text, 0, 1), Node(rule_grammar['_'], text, 1, 1)]))
         text = '?'
-        eq_(rule_grammar['quantifier'].parse(text),
-            Node('quantifier', text, 0, 1, children=[
-                Node('', text, 0, 1), Node('_', text, 1, 1)]))
+        eq_(quantifier.parse(text),
+            Node(quantifier, text, 0, 1, children=[
+                Node(quantifier.members[0], text, 0, 1), Node(rule_grammar['_'], text, 1, 1)]))
         text = '+'
-        eq_(rule_grammar['quantifier'].parse(text),
-            Node('quantifier', text, 0, 1, children=[
-                Node('', text, 0, 1), Node('_', text, 1, 1)]))
+        eq_(quantifier.parse(text),
+            Node(quantifier, text, 0, 1, children=[
+                Node(quantifier.members[0], text, 0, 1), Node(rule_grammar['_'], text, 1, 1)]))
 
     def test_spaceless_literal(self):
         text = '"anything but quotes#$*&^"'
-        eq_(rule_grammar['spaceless_literal'].parse(text),
-            Node('spaceless_literal', text, 0, len(text), children=[
-                Node('', text, 0, len(text))]))
+        spaceless_literal = rule_grammar['spaceless_literal']
+        eq_(spaceless_literal.parse(text),
+            Node(spaceless_literal, text, 0, len(text), children=[
+                Node(spaceless_literal.members[0], text, 0, len(text))]))
         text = r'''r"\""'''
-        eq_(rule_grammar['spaceless_literal'].parse(text),
-            Node('spaceless_literal', text, 0, 5, children=[
-                Node('', text, 0, 5)]))
+        eq_(spaceless_literal.parse(text),
+            Node(spaceless_literal, text, 0, 5, children=[
+                Node(spaceless_literal.members[0], text, 0, 5)]))
 
     def test_regex(self):
         text = '~"[a-zA-Z_][a-zA-Z_0-9]*"LI'
+        regex = rule_grammar['regex']
         eq_(rule_grammar['regex'].parse(text),
-            Node('regex', text, 0, len(text), children=[
-                 Node('', text, 0, 1),
-                 Node('spaceless_literal', text, 1, 25, children=[
-                     Node('', text, 1, 25)]),
-                 Node('', text, 25, 27),
-                 Node('_', text, 27, 27)]))
+            Node(regex, text, 0, len(text), children=[
+                 Node(Literal('~'), text, 0, 1),
+                 Node(rule_grammar['spaceless_literal'], text, 1, 25, children=[
+                     Node(rule_grammar['spaceless_literal'].members[0], text, 1, 25)]),
+                 Node(regex.members[2], text, 25, 27),
+                 Node(rule_grammar['_'], text, 27, 27)]))
 
     def test_successes(self):
         """Make sure the PEG recognition grammar succeeds on various inputs."""
@@ -117,7 +120,7 @@ class RuleVisitorTests(TestCase):
         rules, default_rule = RuleVisitor().visit(tree)
 
         text = '98'
-        eq_(default_rule.parse(text), Node('number', text, 0, 2))
+        eq_(default_rule.parse(text), Node(default_rule, text, 0, 2))
 
     def test_undefined_rule(self):
         """Make sure we throw the right exception on undefined rules."""
@@ -132,8 +135,8 @@ class RuleVisitorTests(TestCase):
 
         # It should turn into a Node from the Optional and another from the
         # Literal within.
-        eq_(default_rule.parse(howdy), Node('boy', howdy, 0, 5, children=[
-                                           Node('', howdy, 0, 5)]))
+        eq_(default_rule.parse(howdy), Node(default_rule, howdy, 0, 5, children=[
+                                           Node(Literal("howdy"), howdy, 0, 5)]))
 
 
 class GrammarTests(TestCase):
@@ -150,8 +153,8 @@ class GrammarTests(TestCase):
         """
         greeting_grammar = Grammar('greeting = "hi" / "howdy"')
         tree = greeting_grammar.parse('hi')
-        eq_(tree, Node('greeting', 'hi', 0, 2, children=[
-                       Node('', 'hi', 0, 2)]))
+        eq_(tree, Node(greeting_grammar['greeting'], 'hi', 0, 2, children=[
+                       Node(Literal('hi'), 'hi', 0, 2)]))
 
     def test_unicode(self):
         """Assert that a ``Grammar`` can convert into a string-formatted series
@@ -179,10 +182,10 @@ class GrammarTests(TestCase):
                           bold_close = "))"
                           """)
         s = ' ((boo))yah'
-        eq_(grammar.match(s, pos=1), Node('bold_text', s, 1, 8, children=[
-                                         Node('bold_open', s, 1, 3),
-                                         Node('text', s, 3, 6),
-                                         Node('bold_close', s, 6, 8)]))
+        eq_(grammar.match(s, pos=1), Node(grammar['bold_text'], s, 1, 8, children=[
+                                         Node(grammar['bold_open'], s, 1, 3),
+                                         Node(grammar['text'], s, 3, 6),
+                                         Node(grammar['bold_close'], s, 6, 8)]))
 
     def test_bad_grammar(self):
         """Constructing a Grammar with bad rules should raise ParseError."""
@@ -232,9 +235,9 @@ class GrammarTests(TestCase):
         assert_raises(ParseError, grammar.parse, 'burp')
 
         s = 'arp'
-        eq_(grammar.parse('arp'), Node('starts_with_a', s, 0, 3, children=[
-                                      Node('', s, 0, 0),
-                                      Node('', s, 0, 3)]))
+        eq_(grammar.parse('arp'), Node(grammar['starts_with_a'], s, 0, 3, children=[
+                                      Node(Lookahead(Literal('a')), s, 0, 0),
+                                      Node(Regex(r'[a-z]+'), s, 0, 3)]))
 
     def test_parens(self):
         grammar = Grammar(r'''sequence = "chitty" (" " "bang")+''')
@@ -313,10 +316,10 @@ class GrammarTests(TestCase):
                     (pos + 1) if text[pos].isdigit() else None)
         s = '[6]'
         eq_(grammar.parse(s),
-            Node('bracketed_digit', s, 0, 3, children=[
-                Node('start', s, 0, 1),
-                Node('digit', s, 1, 2),
-                Node('end', s, 2, 3)]))
+            Node(grammar['bracketed_digit'], s, 0, 3, children=[
+                Node(grammar['start'], s, 0, 1),
+                Node(grammar['digit'], s, 1, 2),
+                Node(grammar['end'], s, 2, 3)]))
 
     def test_complex_custom_rules(self):
         """Run 5-arg custom rules through their paces.
@@ -337,10 +340,10 @@ class GrammarTests(TestCase):
                     grammar['real_digit'].match_core(text, pos, cache, error))
         s = '[6]'
         eq_(grammar.parse(s),
-            Node('bracketed_digit', s, 0, 3, children=[
-                Node('start', s, 0, 1),
-                Node('real_digit', s, 1, 2),
-                Node('end', s, 2, 3)]))
+            Node(grammar['bracketed_digit'], s, 0, 3, children=[
+                Node(grammar['start'], s, 0, 1),
+                Node(grammar['real_digit'], s, 1, 2),
+                Node(grammar['end'], s, 2, 3)]))
 
     def test_lazy_custom_rules(self):
         """Make sure LazyReferences manually shoved into custom rules are
@@ -358,9 +361,9 @@ class GrammarTests(TestCase):
                                 name='forty_five')).default('forty_five')
         s = '45'
         eq_(grammar.parse(s),
-            Node('forty_five', s, 0, 2, children=[
-                Node('four', s, 0, 1),
-                Node('five', s, 1, 2)]))
+            Node(grammar['forty_five'], s, 0, 2, children=[
+                Node(grammar['four'], s, 0, 1),
+                Node(grammar['five'], s, 1, 2)]))
 
     def test_unconnected_custom_rules(self):
         """Make sure custom rules that aren't hooked to any other rules still
@@ -373,7 +376,7 @@ class GrammarTests(TestCase):
         grammar = Grammar(one_char=lambda text, pos: pos + 1).default('one_char')
         s = '4'
         eq_(grammar.parse(s),
-            Node('one_char', s, 0, 1))
+            Node(grammar['one_char'], s, 0, 1))
 
     def test_lazy_default_rule(self):
         """Make sure we get an actual rule set as our default rule, even when
@@ -385,7 +388,7 @@ class GrammarTests(TestCase):
             styled_text = text
             text        = "hi"
             """)
-        eq_(grammar.parse('hi'), Node('text', 'hi', 0, 2))
+        eq_(grammar.parse('hi'), Node(grammar['text'], 'hi', 0, 2))
 
     def test_immutable_grammar(self):
         """Make sure that a Grammar is immutable after being created."""
@@ -431,9 +434,9 @@ class TokenGrammarTests(TestCase):
             token1 = "token1"
             """)
         eq_(grammar.parse(s),
-            Node('foo', s, 0, 2, children=[
-                Node('token1', s, 0, 1),
-                Node('', s, 1, 2)]))
+            Node(grammar['foo'], s, 0, 2, children=[
+                Node(grammar['token1'], s, 0, 1),
+                Node(TokenMatcher('token2'), s, 1, 2)]))
 
     def test_parse_failure(self):
         """Parse failures should work normally with token literals."""
diff --git a/parsimonious/tests/test_nodes.py b/parsimonious/tests/test_nodes.py
index caf9f68..20f1a53 100644
--- a/parsimonious/tests/test_nodes.py
+++ b/parsimonious/tests/test_nodes.py
@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
 from nose import SkipTest
-from nose.tools import eq_, ok_, assert_raises
+from nose.tools import eq_, ok_, assert_raises, assert_in
 
 from parsimonious import Grammar, NodeVisitor, VisitationError, rule
+from parsimonious.expressions import Literal
 from parsimonious.nodes import Node
 
 
@@ -33,25 +34,19 @@ class ExplosiveFormatter(NodeVisitor):
 
 
 def test_visitor():
-    """Assert a tree gets visited correctly.
-
-    We start with a tree from applying this grammar... ::
-
+    """Assert a tree gets visited correctly."""
+    grammar = Grammar(r'''
         bold_text  = bold_open text bold_close
         text       = ~'[a-zA-Z 0-9]*'
         bold_open  = '(('
         bold_close = '))'
-
-    ...to this text::
-
-        ((o hai))
-
-    """
+    ''')
     text = '((o hai))'
-    tree = Node('bold_text', text, 0, 9,
-                [Node('bold_open', text, 0, 2),
-                 Node('text', text, 2, 7),
-                 Node('bold_close', text, 7, 9)])
+    tree = Node(grammar['bold_text'], text, 0, 9,
+                [Node(grammar['bold_open'], text, 0, 2),
+                 Node(grammar['text'], text, 2, 7),
+                 Node(grammar['bold_close'], text, 7, 9)])
+    eq_(grammar.parse(text), tree)
     result = HtmlFormatter().visit(tree)
     eq_(result, '<b>o hai</b>')
 
@@ -59,12 +54,12 @@ def test_visitor():
 def test_visitation_exception():
     assert_raises(VisitationError,
                   ExplosiveFormatter().visit,
-                  Node('boom', '', 0, 0))
+                  Node(Literal(''), '', 0, 0))
 
 
 def test_str():
     """Test str and unicode of ``Node``."""
-    n = Node('text', 'o hai', 0, 5)
+    n = Node(Literal('something', name='text'), 'o hai', 0, 5)
     good = '<Node called "text" matching "o hai">'
     eq_(str(n), good)
 
@@ -73,9 +68,16 @@ def test_repr():
     """Test repr of ``Node``."""
     s = u'hai ö'
     boogie = u'böogie'
-    n = Node(boogie, s, 0, 3, children=[
-            Node('', s, 3, 4), Node('', s, 4, 5)])
-    eq_(repr(n), """s = {hai_o}\nNode({boogie}, s, 0, 3, children=[Node('', s, 3, 4), Node('', s, 4, 5)])""".format(hai_o=repr(s), boogie=repr(boogie)))
+    n = Node(Literal(boogie), s, 0, 3, children=[
+            Node(Literal(' '), s, 3, 4), Node(Literal(u'ö'), s, 4, 5)])
+    eq_(repr(n),
+        str("""s = {hai_o}\nNode({boogie}, s, 0, 3, children=[Node({space}, s, 3, 4), Node({o}, s, 4, 5)])""").format(
+            hai_o=repr(s),
+            boogie=repr(Literal(boogie)),
+            space=repr(Literal(" ")),
+            o=repr(Literal(u"ö")),
+        )
+    )
 
 
 def test_parse_shortcut():
@@ -145,6 +147,43 @@ def test_unwrapped_exceptions():
 
 
 def test_node_inequality():
-    node = Node('text', 'o hai', 0, 5)
+    node = Node(Literal('12345'), 'o hai', 0, 5)
     ok_(node != 5)
     ok_(node != None)
+    ok_(node != Node(Literal('23456'), 'o hai', 0, 5))
+    ok_(not (node != Node(Literal('12345'), 'o hai', 0, 5)))
+
+
+def test_generic_visit_NotImplementedError_unnamed_node():
+    """
+    Test that generic_visit provides informative error messages
+    when visitors are not defined.
+
+    Regression test for https://github.com/erikrose/parsimonious/issues/110
+    """
+    class MyVisitor(NodeVisitor):
+        grammar = Grammar(r'''
+            bar = "b" "a" "r"
+        ''')
+        unwrapped_exceptions = (NotImplementedError, )
+
+    with assert_raises(NotImplementedError) as e:
+        MyVisitor().parse('bar')
+    assert_in('No visitor method was defined for this expression: "b"', str(e.exception))
+
+
+def test_generic_visit_NotImplementedError_named_node():
+    """
+    Test that generic_visit provides informative error messages
+    when visitors are not defined.
+    """
+    class MyVisitor(NodeVisitor):
+        grammar = Grammar(r'''
+            bar = myrule myrule myrule
+            myrule = ~"[bar]"
+        ''')
+        unwrapped_exceptions = (NotImplementedError, )
+
+    with assert_raises(NotImplementedError) as e:
+        MyVisitor().parse('bar')
+    assert_in('No visitor method was defined for this expression: myrule = ~"[bar]"', str(e.exception))