Commits

angri committed e19dd8e

custom path length is finally supported with ext.declarative

Comments (0)

Files changed (2)

sqlamp/__init__.py

     """
     A container for options for one tree table.
 
-    :parameters: see :class:`MPManager`.
+    :param _attach_columns:
+        Bool, if ``True`` then created columns will be automatically
+        attached to the table and indices will be declared right away
+        in the object's constructor. Otherwise columns should
+        be attached later manually and indices should be created
+        by calling :meth:`declare_indices`.
+
+    For other parameters description see :class:`MPManager`.
     """
     def __init__(self,
                  table,
                  depth_field='mp_depth',
                  tree_id_field='mp_tree_id',
                  steplen=None,
-                 pathlen=None):
+                 pathlen=None,
+                 _attach_columns=True):
 
         self.table = table
 
             path_params = {'length': pathlen}
 
         self.path_field = self.check_or_create_field(
-            table, 'path', path_field, PathField, path_params
+            table, 'path', path_field, PathField, _attach_columns, path_params
         )
         self.depth_field = self.check_or_create_field(
-            table, 'depth', depth_field, DepthField
+            table, 'depth', depth_field, DepthField, _attach_columns
         )
         self.tree_id_field = self.check_or_create_field(
-            table, 'tree_id', tree_id_field, TreeIdField
+            table, 'tree_id', tree_id_field, TreeIdField, _attach_columns
         )
         self.fields = (self.path_field, self.depth_field, self.tree_id_field)
 
         self.max_children = len(ALPHABET) ** self.steplen
         self.max_depth = (self.pathlen // self.steplen) + 1
 
+        if _attach_columns:
+            self.declare_indices()
+
+    def declare_indices(self):
+        """
+        Populate object's "indices" property with sqlamp specific indices
+        (as of 0.6 there is only one). Appends created indices to the table.
+        """
         self.indices = [
             sqlalchemy.Index(
-                '__'.join((table.name, self.tree_id_field.name,
+                '__'.join((self.table.name, self.tree_id_field.name,
                            self.path_field.name)),
                 self.tree_id_field,
                 self.path_field,
             ),
         ]
         for index in self.indices:
-            table.append_constraint(index)
+            self.table.append_constraint(index)
 
     @classmethod
-    def check_or_create_field(cls, table, name, field, type_, params=None):
+    def check_or_create_field(cls, table, name, field, type_,
+                              attach, params=None):
         """
         Check field argument (one of `path_field`, `depth_field` and
         `tree_id_field`), convert it from field name to `Column` object
         assert field
         if not isinstance(field, basestring):
             assert isinstance(field, sqlalchemy.Column)
-            assert field.table is table
+            assert not attach or field.table is table
         elif field in table.columns:
             field = table.columns[field]
             if params:
         else:
             field = sqlalchemy.Column(field, type_(**(params or {})),
                                       nullable=False)
-            table.append_column(field)
+            if attach:
+                table.append_column(field)
             return field
         assert isinstance(field.type, type_), \
                "The type of %s field should be %r" % (name, type_)
         # preventing the property from being inherited
         del cls.__mp_manager__
 
+        # After this step mapper and table are created.
+        super(DeclarativeMeta, cls).__init__(name, bases, dct)
+
         opts = {}
-        for opt, default in [
-                ('path_field', 'mp_path'),
-                ('depth_field', 'mp_depth'),
-                ('tree_id_field', 'mp_tree_id'),
-                ('steplen', 3),
-                ('instance_manager_key', '_mp_instance_manager')
-            ]:
+        for opt in ['path_field', 'depth_field', 'tree_id_field',
+                    'steplen', 'pathlen', 'instance_manager_key']:
             optname = '__mp_%s__' % opt
             if hasattr(cls, optname):
                 opts[opt] = getattr(cls, optname)
                 delattr(cls, optname)
-            else:
-                opts[opt] = default
 
-        for field, ftype in [
-                ('path_field', PathField),
-                ('depth_field', DepthField),
-                ('tree_id_field', TreeIdField)
-            ]:
-            assert isinstance(opts[field], basestring)
-            if not hasattr(cls, opts[field]):
-                column = sqlalchemy.Column(ftype(), nullable=False)
+        # Suppressing attaching columns to the table, because
+        # those will be attached when we set the class' property
+        mp_manager = MPManager(cls.__table__, _attach_columns=False, **opts)
+        for field in mp_manager._mp_opts.fields:
+            # Columns get attached to the table here
+            if not hasattr(cls, field.name):
                 # SQLAlchemy 0.5.x needs this:
-                dct[opts[field]] = column
+                dct[field.name] = field
                 # and SQLAlchemy 0.6.x needs this:
-                setattr(cls, opts[field], column)
-        super(DeclarativeMeta, cls).__init__(name, bases, dct)
-        mp_manager = MPManager(cls.__table__, **opts)
+                setattr(cls, field.name, field)
+
+        # Declaring indices manually, as it has to be done after
+        # attaching columns to the table.
+        mp_manager._mp_opts.declare_indices()
+
         setattr(cls, mp_manager_name, mp_manager)
         mapper_ext = mp_manager.mapper_extension
         if hasattr(cls.__mapper__, 'extension'):

tests/functional-tests.py

             __mp_manager__ = 'MP'
             __mp_steplen__ = 5
             __mp_depth_field__ = 'MP_depth'
+            __mp_path_field__ = 'path'
+            path = sqlalchemy.Column(sqlamp.PathField(length=120),
+                                     nullable=False)
             id = sqlalchemy.Column(sqlalchemy.Integer, primary_key=True)
             parent_id = sqlalchemy.Column(sqlalchemy.ForeignKey('node.id'))
             parent = sqlalchemy.orm.relation("Node", remote_side=[id])
             self.sess.commit()
 
             [root, child] = self.sess.query(Node).order_by('id').all()
-            self.assertEqual(root.mp_path, '')
+            self.assertEqual(root.path, '')
             self.assertEqual(root.MP_depth, 0)
-            self.assertEqual(child.mp_path, '00000')
+            self.assertEqual(child.path, '00000')
             self.assertEqual(child.MP_depth, 1)
         finally:
             Node.__table__.delete()
         for root in (self.r1, self.r2):
             self.sess.add(root)
             self.sess.flush()
-            for i in xrange(len(sqlamp.ALPHABET)):
+            for i in range(len(sqlamp.ALPHABET)):
                 self.sess.add(self.Node(pid=root.id))
         self.sess.commit()
         self.sess.expire_all()