diff --git a/.hgtags b/.hgtags
index 601279ab960001fc2472af1e3fdc9a7769b91d72_LmhndGFncw==..26b2c3ca5c0edf6f9e60e5ee0f46a196a3b04909_LmhndGFncw== 100644
--- a/.hgtags
+++ b/.hgtags
@@ -71,3 +71,7 @@
 13cd741f8e144265c15bb225fad863234fc8ce16 rql-debian-version-0.30.1-1
 bb70a998ced6a839fd1e28686e4ff5254feefde3 rql-version-0.31.0
 f4f27e4c588e40ba9c6c9c5d92cb22f74f1207a6 rql-debian-version-0.31.0-1
+6135951b6c7e64cccfc473c69cd025305b4de7e8 rql-version-0.31.1
+543fe6d74b492f198d8bf7dc5e90ff3e3276fa8b rql-debian-version-0.31.1-1
+55af3a14cc29dedab697deabfc7602e140ac3dd8 rql-version-0.31.2
+513a02cce3c22ea14a13f878ccee21c9540a26da rql-debian-version-0.31.2-1
diff --git a/ChangeLog b/ChangeLog
index 601279ab960001fc2472af1e3fdc9a7769b91d72_Q2hhbmdlTG9n..26b2c3ca5c0edf6f9e60e5ee0f46a196a3b04909_Q2hhbmdlTG9n 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,20 @@
 ChangeLog for RQL
 =================
 
---
+2012-03-29  --  0.31.2
+    * #88559: speed up query solutions analysis
+
+    * moved valuable_references from Variable to Referencable, it makes sense for
+      ColumnAliases as well
+
+    * various cleanups
+
+
+
+2012-02-03  --  0.31.1
+    * #87988: fixed bug in simplify with sub-queries
+
+
+
+2011-11-09  --  0.31.0
     * #78681: don't crash on column aliases used in outer join
@@ -5,2 +20,3 @@
     * #78681: don't crash on column aliases used in outer join
+
     * #81394: HAVING support in write queries (INSERT,SET,DELETE)
@@ -6,2 +22,3 @@
     * #81394: HAVING support in write queries (INSERT,SET,DELETE)
+
     * #80799: fix wrong type analysis with 'NOT identity'
@@ -7,3 +24,4 @@
     * #80799: fix wrong type analysis with 'NOT identity'
+
     * when possible, use entity type as translation context of relation
       (break cw < 3.13.10 compat)
@@ -8,4 +26,5 @@
     * when possible, use entity type as translation context of relation
       (break cw < 3.13.10 compat)
+
     * #81817: fix add_type_restriction for cases where some types restriction is already in there
 
@@ -10,3 +29,5 @@
     * #81817: fix add_type_restriction for cases where some types restriction is already in there
 
+
+
 2011-09-07  --  0.30.1
@@ -12,5 +33,4 @@
 2011-09-07  --  0.30.1
-
     * #74727: allow entity types to end with a capitalized letter
       provided they contain a lower-cased letter
 
diff --git a/__init__.py b/__init__.py
index 601279ab960001fc2472af1e3fdc9a7769b91d72_X19pbml0X18ucHk=..26b2c3ca5c0edf6f9e60e5ee0f46a196a3b04909_X19pbml0X18ucHk= 100644
--- a/__init__.py
+++ b/__init__.py
@@ -136,8 +136,8 @@
     def _simplify(self, select):
         # recurse on subqueries first
         for subquery in select.with_:
-            for select in subquery.query.children:
-                self._simplify(select)
+            for subselect in subquery.query.children:
+                self._simplify(subselect)
         rewritten = False
         for var in select.defined_vars.values():
             stinfo = var.stinfo
diff --git a/__pkginfo__.py b/__pkginfo__.py
index 601279ab960001fc2472af1e3fdc9a7769b91d72_X19wa2dpbmZvX18ucHk=..26b2c3ca5c0edf6f9e60e5ee0f46a196a3b04909_X19wa2dpbmZvX18ucHk= 100644
--- a/__pkginfo__.py
+++ b/__pkginfo__.py
@@ -1,5 +1,5 @@
 # pylint: disable-msg=W0622
-# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2004-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of rql.
@@ -20,7 +20,7 @@
 __docformat__ = "restructuredtext en"
 
 modname = "rql"
-numversion = (0, 31, 0)
+numversion = (0, 31, 2)
 version = '.'.join(str(num) for num in numversion)
 
 license = 'LGPL'
@@ -83,8 +83,8 @@
     'logilab-common >= 0.47.0',
     'logilab-database >= 1.6.0',
     'yapps == 2.1.1', # XXX to ensure we don't use the broken pypi version
-    'constraint', # fallback if the gecode compiled module is missing
+    'constraint >= 0.5.0', # fallback if the gecode compiled module is missing
     ]
 
 # links to download yapps2 package that is not (yet) registered in pypi
 dependency_links = [
@@ -87,6 +87,6 @@
     ]
 
 # links to download yapps2 package that is not (yet) registered in pypi
 dependency_links = [
-    "http://ftp.logilab.org/pub/yapps/yapps2-2.1.1.zip#egg=yapps-2.1.1",
+    "http://download.logilab.org/pub/yapps/yapps2-2.1.1.zip#egg=yapps-2.1.1",
     ]
diff --git a/analyze.py b/analyze.py
index 601279ab960001fc2472af1e3fdc9a7769b91d72_YW5hbHl6ZS5weQ==..26b2c3ca5c0edf6f9e60e5ee0f46a196a3b04909_YW5hbHl6ZS5weQ== 100644
--- a/analyze.py
+++ b/analyze.py
@@ -22,6 +22,7 @@
 
 from cStringIO import StringIO
 
+import os, sys
 from rql import TypeResolverException, nodes
 from pprint import pprint
 
@@ -29,6 +30,9 @@
 from itertools import izip
 
 try:
+    pure = bool(os.environ.get('RQL_USE_PURE_PYTHON_ANALYSE', 0))
+    if pure:
+        raise ImportError
     import rql_solve
 except ImportError:
     rql_solve = None
@@ -316,9 +320,9 @@
     def set_schema(self, schema):
         self.schema = schema
         # default domains for a variable
-        self._base_domain = [str(etype) for etype in schema.entities()]
-        self._nonfinal_domain = [str(etype) for etype in schema.entities()
-                                 if not etype.final]
+        self._base_domain = set(str(etype) for etype in schema.entities())
+        self._nonfinal_domain = set(str(etype) for etype in schema.entities()
+                                    if not etype.final)
 
     def solve(self, node, constraints):
         # debug info
@@ -339,7 +343,11 @@
         node.set_possible_types(sols, self.kwargs, self.var_solkey)
 
     def _visit(self, node, constraints=None):
-        """Recurse down the tree."""
+        """Recurse down the tree.
+
+            * node: rql node to process
+            * constraints: a XxxCSPProblem object.
+        """
         func = getattr(self, 'visit_%s' % node.__class__.__name__.lower())
         if constraints is None:
             func(node)
@@ -343,10 +351,9 @@
         func = getattr(self, 'visit_%s' % node.__class__.__name__.lower())
         if constraints is None:
             func(node)
-        else:
-            if func(node, constraints) is None:
-                for c in node.children:
-                    self._visit(c, constraints)
+        elif func(node, constraints) is None:
+            for c in node.children:
+                self._visit(c, constraints)
 
     def _uid_node_types(self, valnode):
         types = set()
@@ -505,10 +512,16 @@
             # filter according to domain necessary for column aliases
             rhsdomain = constraints.domains[rhsvar]
             res = []
-            for fromtype, totypes in rschema.associations():
-                if not fromtype in lhsdomain:
-                    continue
-                ptypes = [str(t) for t in totypes if t in rhsdomain]
-                res.append( [ ([lhsvar], [str(fromtype)]),
-                              ([rhsvar], ptypes) ] )
+            var_types = []
+            same_var = (rhsvar == lhsvar)
+
+            for frometype, toetypes in rschema.associations():
+                fromtype = str(frometype)
+                if fromtype in lhsdomain:
+                    totypes = set(str(t) for t in toetypes)
+                    ptypes = totypes & rhsdomain
+                    res.append( [ ([lhsvar], [str(fromtype)]),
+                                  ([rhsvar], list(ptypes)) ] )
+                    if same_var and (fromtype in totypes): #ptypes ?
+                        var_types.append(fromtype)
             constraints.or_and(res)
@@ -514,8 +527,6 @@
             constraints.or_and(res)
-            if rhsvar == lhsvar:
-                res = [str(fromtype) for fromtype, totypes in rschema.associations()
-                       if (fromtype in totypes and fromtype in lhsdomain)]
-                constraints.var_has_types( lhsvar, res )
+            if same_var:
+                constraints.var_has_types( lhsvar, var_types)
         else:
             # XXX consider rhs.get_type?
             lhsdomain = constraints.domains[lhs.name]
diff --git a/base.py b/base.py
index 601279ab960001fc2472af1e3fdc9a7769b91d72_YmFzZS5weQ==..26b2c3ca5c0edf6f9e60e5ee0f46a196a3b04909_YmFzZS5weQ== 100644
--- a/base.py
+++ b/base.py
@@ -22,4 +22,5 @@
 
 __docformat__ = "restructuredtext en"
 
+from rql.utils import VisitableMixIn
 
@@ -25,5 +26,6 @@
 
-class BaseNode(object):
+
+class BaseNode(VisitableMixIn):
     __slots__ = ('parent',)
 
     def __str__(self):
diff --git a/debian/changelog b/debian/changelog
index 601279ab960001fc2472af1e3fdc9a7769b91d72_ZGViaWFuL2NoYW5nZWxvZw==..26b2c3ca5c0edf6f9e60e5ee0f46a196a3b04909_ZGViaWFuL2NoYW5nZWxvZw== 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,15 @@
+rql (0.31.2-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Thu, 29 Mar 2012 13:58:45 +0200
+
+rql (0.31.1-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Fri, 03 Feb 2012 16:10:56 +0100
+
 rql (0.31.0-1) unstable; urgency=low
 
   * new upstream release
diff --git a/debian/control b/debian/control
index 601279ab960001fc2472af1e3fdc9a7769b91d72_ZGViaWFuL2NvbnRyb2w=..26b2c3ca5c0edf6f9e60e5ee0f46a196a3b04909_ZGViaWFuL2NvbnRyb2w= 100644
--- a/debian/control
+++ b/debian/control
@@ -3,7 +3,12 @@
 Priority: optional
 Maintainer: Logilab Packaging Team <contact@logilab.fr>
 Uploaders: Sylvain Thenault <sylvain.thenault@logilab.fr>, Nicolas Chauvat <nicolas.chauvat@logilab.fr>
-Build-Depends: debhelper (>= 5.0.37.1), python-support, python-all-dev (>=2.5), python-all (>=2.5), libgecode-dev, python-sphinx, g++ (>= 4)
+Build-Depends: debhelper (>= 5.0.37.1),
+               python-support,
+               python-all-dev (>=2.5),
+               python-all (>=2.5),
+               libgecode-dev,
+               python-sphinx, g++ (>= 4)
 XS-Python-Version: >= 2.5
 Standards-Version: 3.9.1
 Homepage: http://www.logilab.org/project/rql
@@ -11,7 +16,10 @@
 Package: python-rql
 Architecture: any
 XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, ${misc:Depends}, ${shlibs:Depends}, python-logilab-common (>= 0.35.3-1), yapps2-runtime, python-logilab-database (>= 1.6.0)
+Depends: ${python:Depends}, ${misc:Depends}, ${shlibs:Depends},
+         python-logilab-common (>= 0.35.3-1),
+         yapps2-runtime,
+         python-logilab-database (>= 1.6.0)
 Conflicts: cubicweb-common (<= 3.13.9)
 Provides: ${python:Provides}
 Description: relationship query language (RQL) utilities
diff --git a/nodes.py b/nodes.py
index 601279ab960001fc2472af1e3fdc9a7769b91d72_bm9kZXMucHk=..26b2c3ca5c0edf6f9e60e5ee0f46a196a3b04909_bm9kZXMucHk= 100644
--- a/nodes.py
+++ b/nodes.py
@@ -1,4 +1,4 @@
-# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2004-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of rql.
@@ -32,8 +32,8 @@
 
 from rql import CoercionError, RQLException
 from rql.base import BaseNode, Node, BinaryNode, LeafNode
-from rql.utils import (function_description, quote, uquote, build_visitor_stub,
-                       common_parent)
+from rql.utils import (function_description, quote, uquote, common_parent,
+                       VisitableMixIn)
 
 CONSTANT_TYPES = frozenset((None, 'Date', 'Datetime', 'Boolean', 'Float', 'Int',
                             'String', 'Substitute', 'etype'))
@@ -901,7 +901,7 @@
 
 ###############################################################################
 
-class Referenceable(object):
+class Referenceable(VisitableMixIn):
     __slots__ = ('name', 'stinfo', 'stmt')
 
     def __init__(self, name):
@@ -1076,6 +1076,13 @@
                 return rel
         return None
 
+    def valuable_references(self):
+        """return the number of "valuable" references :
+        references is in selection or in a non type (is) relations
+        """
+        stinfo = self.stinfo
+        return len(stinfo['selected']) + len(stinfo['relations'])
+
 
 class ColumnAlias(Referenceable):
     __slots__ = ('colnum', 'query',
@@ -1125,10 +1132,4 @@
     def __repr__(self):
         return '%s(%#X)' % (self.name, id(self))
 
-    def valuable_references(self):
-        """return the number of "valuable" references :
-        references is in selection or in a non type (is) relations
-        """
-        stinfo = self.stinfo
-        return len(stinfo['selected']) + len(stinfo['relations'])
 
@@ -1134,5 +1135,1 @@
 
-
-build_visitor_stub((SubQuery, And, Or, Not, Exists, Relation,
-                    Comparison, MathExpression, UnaryExpression, Function,
-                    Constant, VariableRef, SortTerm, ColumnAlias, Variable))
diff --git a/stmts.py b/stmts.py
index 601279ab960001fc2472af1e3fdc9a7769b91d72_c3RtdHMucHk=..26b2c3ca5c0edf6f9e60e5ee0f46a196a3b04909_c3RtdHMucHk= 100644
--- a/stmts.py
+++ b/stmts.py
@@ -31,7 +31,7 @@
 
 from rql import BadRQLQuery, CoercionError, nodes
 from rql.base import BaseNode, Node
-from rql.utils import rqlvar_maker, build_visitor_stub
+from rql.utils import rqlvar_maker
 
 _MARKER = object()
 
@@ -1113,4 +1113,3 @@
         return new
 
 
-build_visitor_stub((Union, Select, Insert, Delete, Set))
diff --git a/test/unittest_parser.py b/test/unittest_parser.py
index 601279ab960001fc2472af1e3fdc9a7769b91d72_dGVzdC91bml0dGVzdF9wYXJzZXIucHk=..26b2c3ca5c0edf6f9e60e5ee0f46a196a3b04909_dGVzdC91bml0dGVzdF9wYXJzZXIucHk= 100644
--- a/test/unittest_parser.py
+++ b/test/unittest_parser.py
@@ -187,7 +187,7 @@
         tree = self.parse(u"Any X WHERE X name 'Ångström';")
         base = tree.children[0].where
         comparison = base.children[1]
-        self.failUnless(isinstance(comparison, nodes.Comparison))
+        self.assertTrue(isinstance(comparison, nodes.Comparison))
         rhs = comparison.children[0]
         self.assertEqual(type(rhs.value), unicode)
 
@@ -306,8 +306,8 @@
                          "Any X WHERE X firstname 'lulu', "
                          "EXISTS(X owned_by U, U in_group G, (G name 'lulufanclub') OR (G name 'managers'))")
         exists = tree.children[0].where.get_nodes(nodes.Exists)[0]
-        self.failUnless(exists.children[0].parent is exists)
-        self.failUnless(exists.parent)
+        self.assertTrue(exists.children[0].parent is exists)
+        self.assertTrue(exists.parent)
 
     def test_etype(self):
         tree = self.parse('EmailAddress X;')
diff --git a/test/unittest_stcheck.py b/test/unittest_stcheck.py
index 601279ab960001fc2472af1e3fdc9a7769b91d72_dGVzdC91bml0dGVzdF9zdGNoZWNrLnB5..26b2c3ca5c0edf6f9e60e5ee0f46a196a3b04909_dGVzdC91bml0dGVzdF9zdGNoZWNrLnB5 100644
--- a/test/unittest_stcheck.py
+++ b/test/unittest_stcheck.py
@@ -1,4 +1,4 @@
-# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2004-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of rql.
@@ -225,8 +225,8 @@
         self.simplify(tree)
         copy = tree.copy()
         exists = copy.get_nodes(nodes.Exists)[0]
-        self.failUnless(exists.children[0].parent is exists)
-        self.failUnless(exists.parent)
+        self.assertTrue(exists.children[0].parent is exists)
+        self.assertTrue(exists.parent)
 
     def test_copy_internals(self):
         root = self.parse('Any X,U WHERE C owned_by U, NOT X owned_by U, X eid 1, C eid 2')
@@ -254,7 +254,7 @@
 #     def test_simplified(self):
 #         rqlst = self.parse('Any L WHERE 5 name L')
 #         self.annotate(rqlst)
-#         self.failUnless(rqlst.defined_vars['L'].stinfo['attrvar'])
+#         self.assertTrue(rqlst.defined_vars['L'].stinfo['attrvar'])
 
     def test_is_rel_no_scope_1(self):
         """is relation used as type restriction should not affect variable's
@@ -262,9 +262,9 @@
         """
         rqlst = self.parse('Any X WHERE C is Company, EXISTS(X work_for C)').children[0]
         C = rqlst.defined_vars['C']
-        self.failIf(C.scope is rqlst, C.scope)
+        self.assertFalse(C.scope is rqlst, C.scope)
         self.assertEqual(len(C.stinfo['relations']), 1)
 
     def test_is_rel_no_scope_2(self):
         rqlst = self.parse('Any X, ET WHERE C is ET, EXISTS(X work_for C)').children[0]
         C = rqlst.defined_vars['C']
@@ -266,9 +266,9 @@
         self.assertEqual(len(C.stinfo['relations']), 1)
 
     def test_is_rel_no_scope_2(self):
         rqlst = self.parse('Any X, ET WHERE C is ET, EXISTS(X work_for C)').children[0]
         C = rqlst.defined_vars['C']
-        self.failUnless(C.scope is rqlst, C.scope)
+        self.assertTrue(C.scope is rqlst, C.scope)
         self.assertEqual(len(C.stinfo['relations']), 2)
 
 
@@ -276,9 +276,9 @@
         rqlst = self.parse('Any X WHERE C is Company, NOT X work_for C').children[0]
         self.assertEqual(rqlst.as_string(), 'Any X WHERE C is Company, NOT EXISTS(X work_for C)')
         C = rqlst.defined_vars['C']
-        self.failIf(C.scope is rqlst, C.scope)
+        self.assertFalse(C.scope is rqlst, C.scope)
 
     def test_not_rel_normalization_2(self):
         rqlst = self.parse('Any X, ET WHERE C is ET, NOT X work_for C').children[0]
         self.assertEqual(rqlst.as_string(), 'Any X,ET WHERE C is ET, NOT EXISTS(X work_for C)')
         C = rqlst.defined_vars['C']
@@ -280,11 +280,11 @@
 
     def test_not_rel_normalization_2(self):
         rqlst = self.parse('Any X, ET WHERE C is ET, NOT X work_for C').children[0]
         self.assertEqual(rqlst.as_string(), 'Any X,ET WHERE C is ET, NOT EXISTS(X work_for C)')
         C = rqlst.defined_vars['C']
-        self.failUnless(C.scope is rqlst, C.scope)
+        self.assertTrue(C.scope is rqlst, C.scope)
 
     def test_not_rel_normalization_3(self):
         rqlst = self.parse('Any X WHERE C is Company, X work_for C, NOT C name "World Company"').children[0]
         self.assertEqual(rqlst.as_string(), "Any X WHERE C is Company, X work_for C, NOT C name 'World Company'")
         C = rqlst.defined_vars['C']
@@ -286,11 +286,11 @@
 
     def test_not_rel_normalization_3(self):
         rqlst = self.parse('Any X WHERE C is Company, X work_for C, NOT C name "World Company"').children[0]
         self.assertEqual(rqlst.as_string(), "Any X WHERE C is Company, X work_for C, NOT C name 'World Company'")
         C = rqlst.defined_vars['C']
-        self.failUnless(C.scope is rqlst, C.scope)
+        self.assertTrue(C.scope is rqlst, C.scope)
 
     def test_not_rel_normalization_4(self):
         rqlst = self.parse('Any X WHERE C is Company, NOT (X work_for C, C name "World Company")').children[0]
         self.assertEqual(rqlst.as_string(), "Any X WHERE C is Company, NOT EXISTS(X work_for C, C name 'World Company')")
         C = rqlst.defined_vars['C']
@@ -292,11 +292,11 @@
 
     def test_not_rel_normalization_4(self):
         rqlst = self.parse('Any X WHERE C is Company, NOT (X work_for C, C name "World Company")').children[0]
         self.assertEqual(rqlst.as_string(), "Any X WHERE C is Company, NOT EXISTS(X work_for C, C name 'World Company')")
         C = rqlst.defined_vars['C']
-        self.failIf(C.scope is rqlst, C.scope)
+        self.assertFalse(C.scope is rqlst, C.scope)
 
     def test_not_rel_normalization_5(self):
         rqlst = self.parse('Any X WHERE X work_for C, EXISTS(C identity D, NOT Y work_for D, D name "World Company")').children[0]
         self.assertEqual(rqlst.as_string(), "Any X WHERE X work_for C, EXISTS(C identity D, NOT EXISTS(Y work_for D), D name 'World Company')")
         D = rqlst.defined_vars['D']
@@ -298,11 +298,11 @@
 
     def test_not_rel_normalization_5(self):
         rqlst = self.parse('Any X WHERE X work_for C, EXISTS(C identity D, NOT Y work_for D, D name "World Company")').children[0]
         self.assertEqual(rqlst.as_string(), "Any X WHERE X work_for C, EXISTS(C identity D, NOT EXISTS(Y work_for D), D name 'World Company')")
         D = rqlst.defined_vars['D']
-        self.failIf(D.scope is rqlst, D.scope)
-        self.failUnless(D.scope.parent.scope is rqlst, D.scope.parent.scope)
+        self.assertFalse(D.scope is rqlst, D.scope)
+        self.assertTrue(D.scope.parent.scope is rqlst, D.scope.parent.scope)
 
     def test_subquery_annotation_1(self):
         rqlst = self.parse('Any X WITH X BEING (Any X WHERE C is Company, EXISTS(X work_for C))').children[0]
         C = rqlst.with_[0].query.children[0].defined_vars['C']
@@ -305,10 +305,10 @@
 
     def test_subquery_annotation_1(self):
         rqlst = self.parse('Any X WITH X BEING (Any X WHERE C is Company, EXISTS(X work_for C))').children[0]
         C = rqlst.with_[0].query.children[0].defined_vars['C']
-        self.failIf(C.scope is rqlst, C.scope)
+        self.assertFalse(C.scope is rqlst, C.scope)
         self.assertEqual(len(C.stinfo['relations']), 1)
 
     def test_subquery_annotation_2(self):
         rqlst = self.parse('Any X,ET WITH X,ET BEING (Any X, ET WHERE C is ET, EXISTS(X work_for C))').children[0]
         C = rqlst.with_[0].query.children[0].defined_vars['C']
@@ -310,8 +310,8 @@
         self.assertEqual(len(C.stinfo['relations']), 1)
 
     def test_subquery_annotation_2(self):
         rqlst = self.parse('Any X,ET WITH X,ET BEING (Any X, ET WHERE C is ET, EXISTS(X work_for C))').children[0]
         C = rqlst.with_[0].query.children[0].defined_vars['C']
-        self.failUnless(C.scope is rqlst.with_[0].query.children[0], C.scope)
+        self.assertTrue(C.scope is rqlst.with_[0].query.children[0], C.scope)
         self.assertEqual(len(C.stinfo['relations']), 2)
         X = rqlst.get_variable('X')
@@ -316,6 +316,6 @@
         self.assertEqual(len(C.stinfo['relations']), 2)
         X = rqlst.get_variable('X')
-        self.failUnless(X.scope is rqlst, X.scope)
+        self.assertTrue(X.scope is rqlst, X.scope)
 
     def test_no_attr_var_if_uid_rel(self):
         with self.assertRaises(BadRQLQuery) as cm:
diff --git a/utils.py b/utils.py
index 601279ab960001fc2472af1e3fdc9a7769b91d72_dXRpbHMucHk=..26b2c3ca5c0edf6f9e60e5ee0f46a196a3b04909_dXRpbHMucHk= 100644
--- a/utils.py
+++ b/utils.py
@@ -133,7 +133,7 @@
     SQL_FUNCTIONS_REGISTRY.register_function(funcdef)
 
 def function_description(funcname):
-    """Return the description (`FunctionDescription`) for a RQL function."""
+    """Return the description (:class:`FunctionDescr`) for a RQL function."""
     return RQL_FUNCTIONS_REGISTRY.get_function(funcname)
 
 def quote(value):
@@ -156,5 +156,7 @@
     res.append(u'"')
     return u''.join(res)
 
-# Visitor #####################################################################
+
+
+class VisitableMixIn(object):
 
@@ -160,10 +162,13 @@
 
-_accept = 'lambda self, visitor, *args, **kwargs: visitor.visit_%s(self, *args, **kwargs)'
-_leave = 'lambda self, visitor, *args, **kwargs: visitor.leave_%s(self, *args, **kwargs)'
-def build_visitor_stub(classes):
-    for cls in classes:
-        cls.accept = eval(_accept % (cls.__name__.lower()))
-        cls.leave = eval(_leave % (cls.__name__.lower()))
+    def accept(self, visitor, *args, **kwargs):
+        visit_id = self.__class__.__name__.lower()
+        visit_method = getattr(visitor, 'visit_%s' % visit_id)
+        return visit_method(self, *args, **kwargs)
+
+    def leave(self, visitor, *args, **kwargs):
+        visit_id = self.__class__.__name__.lower()
+        visit_method = getattr(visitor, 'leave_%s' % visit_id)
+        return visit_method(self, *args, **kwargs)
 
 class RQLVisitorHandler(object):
     """Handler providing a dummy implementation of all callbacks necessary