]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Added support for @classproperty to provide
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 3 Jul 2010 18:53:37 +0000 (14:53 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 3 Jul 2010 18:53:37 +0000 (14:53 -0400)
any kind of schema/mapping construct from a
declarative mixin, including columns with foreign
keys, relationships, column_property, deferred.
This solves all such issues on declarative mixins.
An error is raised if any MapperProperty subclass
is specified on a mixin without using @classproperty.
[ticket:1751] [ticket:1796] [ticket:1805]

- un-anglicized the declarative docs

CHANGES
doc/build/reference/orm/mapping.rst
lib/sqlalchemy/ext/declarative.py
test/ext/test_declarative.py

diff --git a/CHANGES b/CHANGES
index c7e4251db2a15b8a61d1f9d5ffa2dd8e4cbddd8d..11ae0e575134c6f9984086e7d71a831b5dac6501 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -165,6 +165,15 @@ CHANGES
     "CHARACTER SET" clause.  [ticket:1813]
   
 - declarative
+   - Added support for @classproperty to provide 
+     any kind of schema/mapping construct from a 
+     declarative mixin, including columns with foreign 
+     keys, relationships, column_property, deferred.
+     This solves all such issues on declarative mixins.
+     An error is raised if any MapperProperty subclass
+     is specified on a mixin without using @classproperty.
+     [ticket:1751] [ticket:1796] [ticket:1805]
+     
    - a mixin class can now define a column that matches
      one which is present on a __table__ defined on a 
      subclass.  It cannot, however, define one that is 
index b8842d8dc8b12b3d0dfdc84297891aaf90fe0a2c..8bab92e5901715889086272b98100cf0129b38fa 100644 (file)
@@ -91,3 +91,6 @@ Internals
 
 .. autoclass:: sqlalchemy.orm.mapper.Mapper
    :members:
+
+.. autoclass:: sqlalchemy.orm.interfaces.MapperProperty
+  :members:
index 7ff6051460dcaea9d32b213083836599f2d19f59..d0a02381d3e8726753fcaa9434f420c5eb1c71dc 100755 (executable)
@@ -82,7 +82,7 @@ automatically::
     Base = declarative_base(bind=create_engine('sqlite://'))
 
 Alternatively, by way of the normal
-:class:`~sqlalchemy.schema.MetaData` behaviour, the ``bind`` attribute
+:class:`~sqlalchemy.schema.MetaData` behavior, the ``bind`` attribute
 of the class level accessor can be assigned at any time as follows::
 
     Base.metadata.bind = create_engine('sqlite://')
@@ -101,10 +101,7 @@ Configuring Relationships
 
 Relationships to other classes are done in the usual way, with the added
 feature that the class specified to :func:`~sqlalchemy.orm.relationship`
-may be a string name (note that :func:`~sqlalchemy.orm.relationship` is 
-only available as of SQLAlchemy 0.6beta2, and in all prior versions is known
-as :func:`~sqlalchemy.orm.relation`, 
-including 0.5 and 0.4).  The "class registry" associated with ``Base``
+may be a string name.  The "class registry" associated with ``Base``
 is used at mapper compilation time to resolve the name into the actual
 class object, which is expected to have been defined once the mapper
 configuration is used:: 
@@ -421,74 +418,199 @@ requires usage of :func:`~sqlalchemy.orm.util.polymorphic_union`::
         __mapper_args__ = {'polymorphic_identity':'manager', 'concrete':True}
     
 
-Mix-in Classes
+Mixin Classes
 ==============
 
 A common need when using :mod:`~sqlalchemy.ext.declarative` is to
 share some functionality, often a set of columns, across many
-classes. The normal python idiom would be to put this common code into
+classes. The normal Python idiom would be to put this common code into
 a base class and have all the other classes subclass this class.
 
 When using :mod:`~sqlalchemy.ext.declarative`, this need is met by
-using a "mix-in class". A mix-in class is one that isn't mapped to a
+using a "mixin class". A mixin class is one that isn't mapped to a
 table and doesn't subclass the declarative :class:`Base`. For example::
 
     class MyMixin(object):
     
-        __table_args__ = {'mysql_engine':'InnoDB'}             
-        __mapper_args__=dict(always_refresh=True)
+        __table_args__ = {'mysql_engine': 'InnoDB'}
+        __mapper_args__= {'always_refresh': True}
+        
         id =  Column(Integer, primary_key=True)
 
-        def foo(self):
-            return 'bar'+str(self.id)
 
     class MyModel(Base,MyMixin):
-        __tablename__='test'
-        name = Column(String(1000), nullable=False, index=True)
+        __tablename__ = 'test'
 
-As the above example shows, ``__table_args__`` and ``__mapper_args__``
-can both be abstracted out into a mix-in if you use common values for
-these across many classes.
+        name = Column(String(1000))
 
-However, particularly in the case of ``__table_args__``, you may want
-to combine some parameters from several mix-ins with those you wish to
-define on the class iteself. To help with this, a
-:func:`~sqlalchemy.util.classproperty` decorator is provided that lets
-you implement a class property with a function. For example::
+Where above, the class ``MyModel`` will contain an "id" column
+as well as ``__table_args__`` and ``__mapper_args__`` defined
+by the ``MyMixin`` mixin class.
 
-    from sqlalchemy.util import classproperty
+Mixing in Columns
+~~~~~~~~~~~~~~~~~
 
-    class MySQLSettings:
-        __table_args__ = {'mysql_engine':'InnoDB'}             
+The most basic way to specify a column on a mixin is by simple 
+declaration::
 
-    class MyOtherMixin:
-        __table_args__ = {'info':'foo'}
+    class TimestampMixin(object):
+        created_at = Column(DateTime, default=func.now())
 
-    class MyModel(Base,MySQLSettings,MyOtherMixin):
-        __tablename__='my_model'
+    class MyModel(Base, TimestampMixin):
+        __tablename__ = 'test'
 
+        id =  Column(Integer, primary_key=True)
+        name = Column(String(1000))
+
+Where above, all declarative classes that include ``TimestampMixin``
+will also have a column ``created_at`` that applies a timestamp to 
+all row insertions.
+
+Those familiar with the SQLAlchemy expression language know that 
+the object identity of clause elements defines their role in a schema.
+Two ``Table`` objects ``a`` and ``b`` may both have a column called 
+``id``, but the way these are differentiated is that ``a.c.id`` 
+and ``b.c.id`` are two distinct Python objects, referencing their
+parent tables ``a`` and ``b`` respectively.
+
+In the case of the mixin column, it seems that only one
+:class:`Column` object is explicitly created, yet the ultimate 
+``created_at`` column above must exist as a distinct Python object
+for each separate destination class.  To accomplish this, the declarative
+extension creates a **copy** of each :class:`Column` object encountered on 
+a class that is detected as a mixin.
+
+This copy mechanism is limited to simple columns that have no foreign
+keys, as a :class:`ForeignKey` itself contains references to columns
+which can't be properly recreated at this level.  For columns that 
+have foreign keys, as well as for the variety of mapper-level constructs
+that require destination-explicit context, the
+:func:`~sqlalchemy.util.classproperty` decorator is provided so that
+patterns common to many classes can be defined as callables::
+
+    from sqlalchemy.util import classproperty
+    
+    class ReferenceAddressMixin(object):
         @classproperty
-        def __table_args__(self):
-            args = dict()
-            args.update(MySQLSettings.__table_args__)
-            args.update(MyOtherMixin.__table_args__)
-            return args
+        def address_id(cls):
+            return Column(Integer, ForeignKey('address.id'))
+            
+    class User(Base, ReferenceAddressMixin):
+        __tablename__ = 'user'
+        id = Column(Integer, primary_key=True)
+        
+Where above, the ``address_id`` class-level callable is executed at the 
+point at which the ``User`` class is constructed, and the declarative
+extension can use the resulting :class:`Column` object as returned by
+the method without the need to copy it.
+
+Columns generated by :func:`~sqlalchemy.util.classproperty` can also be
+referenced by ``__mapper_args__`` to a limited degree, currently 
+by ``polymorphic_on`` and ``version_id_col``, by specifying the 
+classdecorator itself into the dictionary - the declarative extension
+will resolve them at class construction time::
+
+    class MyMixin:
+        @classproperty
+        def type_(cls):
+            return Column(String(50))
 
+        __mapper_args__= {'polymorphic_on':type_}
+
+    class MyModel(Base,MyMixin):
+        __tablename__='test'
         id =  Column(Integer, primary_key=True)
+    
+.. note:: The usage of :func:`~sqlalchemy.util.classproperty` with mixin 
+   columns is a new feature as of SQLAlchemy 0.6.2.
+
+Mixing in Relationships
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Relationships created by :func:`~sqlalchemy.orm.relationship` are provided
+exclusively using the :func:`~sqlalchemy.util.classproperty` approach,
+eliminating any ambiguity which could arise when copying a relationship
+and its possibly column-bound contents.  Below is an example which 
+combines a foreign key column and a relationship so that two classes
+``Foo`` and ``Bar`` can both be configured to reference a common
+target class via many-to-one::
+
+    class RefTargetMixin(object):
+        @classproperty
+        def target_id(cls):
+            return Column('target_id', ForeignKey('target.id'))
+    
+        @classproperty
+        def target(cls):
+            return relationship("Target")
+    
+    class Foo(Base, RefTargetMixin):
+        __tablename__ = 'foo'
+        id = Column(Integer, primary_key=True)
+    
+    class Bar(Base, RefTargetMixin):
+        __tablename__ = 'bar'
+        id = Column(Integer, primary_key=True)
+                    
+    class Target(Base):
+        __tablename__ = 'target'
+        id = Column(Integer, primary_key=True)
 
-Controlling table inheritance with mix-ins
+:func:`~sqlalchemy.orm.relationship` definitions which require explicit
+primaryjoin, order_by etc. expressions should use the string forms 
+for these arguments, so that they are evaluated as late as possible.  
+To reference the mixin class in these expressions, use the given ``cls``
+to get it's name::
+
+    class RefTargetMixin(object):
+        @classproperty
+        def target_id(cls):
+            return Column('target_id', ForeignKey('target.id'))
+        
+        @classproperty
+        def target(cls):
+            return relationship("Target",
+                primaryjoin="Target.id==%s.target_id" % cls.__name__
+            )
+
+.. note:: The usage of :func:`~sqlalchemy.util.classproperty` with mixin 
+   relationships is a new feature as of SQLAlchemy 0.6.2.
+
+
+Mixing in deferred(), column_property(), etc.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Like :func:`~sqlalchemy.orm.relationship`, all :class:`~sqlalchemy.orm.interfaces.MapperProperty`
+subclasses such as :func:`~sqlalchemy.orm.deferred`, 
+:func:`~sqlalchemy.orm.column_property`, etc. ultimately involve references
+to columns, and therefore have the :func:`~sqlalchemy.util.classproperty` requirement so that no reliance on copying is needed::
+
+    class SomethingMixin(object):
+
+        @classproperty
+        def dprop(cls):
+            return deferred(Column(Integer))
+
+    class Something(Base, SomethingMixin):
+        __tablename__ = "something"
+
+.. note:: The usage of :func:`~sqlalchemy.util.classproperty` with mixin 
+   mapper properties is a new feature as of SQLAlchemy 0.6.2.
+
+
+Controlling table inheritance with mixins
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 The ``__tablename__`` attribute in conjunction with the hierarchy of
 the classes involved controls what type of table inheritance, if any,
 is configured by the declarative extension.
 
-If the ``__tablename__`` is computed by a mix-in, you may need to
+If the ``__tablename__`` is computed by a mixin, you may need to
 control which classes get the computed attribute in order to get the
 type of table inheritance you require.
 
-For example, if you had a mix-in that computes ``__tablename__`` but
-where you wanted to use that mix-in in a single table inheritance
+For example, if you had a mixin that computes ``__tablename__`` but
+where you wanted to use that mixin in a single table inheritance
 hierarchy, you can explicitly specify ``__tablename__`` as ``None`` to
 indicate that the class should not have a table mapped::
 
@@ -509,14 +631,14 @@ indicate that the class should not have a table mapped::
         __mapper_args__ = {'polymorphic_identity': 'engineer'}
         primary_language = Column(String(50))
 
-Alternatively, you can make the mix-in intelligent enough to only
+Alternatively, you can make the mixin intelligent enough to only
 return a ``__tablename__`` in the event that no table is already
 mapped in the inheritance hierarchy. To help with this, a
 :func:`~sqlalchemy.ext.declarative.has_inherited_table` helper
 function is provided that returns ``True`` if a parent class already
 has a mapped table. 
 
-As an examply, here's a mix-in that will only allow single table
+As an example, here's a mixin that will only allow single table
 inheritance::
 
     from sqlalchemy.util import classproperty
@@ -540,7 +662,7 @@ inheritance::
         primary_language = Column(String(50))
 
 If you want to use a similar pattern with a mix of single and joined
-table inheritance, you would need a slightly different mix-in and use
+table inheritance, you would need a slightly different mixin and use
 it on any joined table child classes in addition to their parent
 classes::
 
@@ -572,6 +694,36 @@ classes::
         __mapper_args__ = {'polymorphic_identity': 'engineer'}
         preferred_recreation = Column(String(50))
 
+Combining Table/Mapper Arguments from Multiple Mixins
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In the case of ``__table_args__`` or ``__mapper_args__``, you may want
+to combine some parameters from several mixins with those you wish to
+define on the class iteself.  The 
+:func:`~sqlalchemy.util.classproperty` decorator can be used here
+to create user-defined collation routines that pull from multiple
+collections::
+
+    from sqlalchemy.util import classproperty
+
+    class MySQLSettings:
+        __table_args__ = {'mysql_engine':'InnoDB'}             
+
+    class MyOtherMixin:
+        __table_args__ = {'info':'foo'}
+
+    class MyModel(Base,MySQLSettings,MyOtherMixin):
+        __tablename__='my_model'
+
+        @classproperty
+        def __table_args__(self):
+            args = dict()
+            args.update(MySQLSettings.__table_args__)
+            args.update(MyOtherMixin.__table_args__)
+            return args
+
+        id =  Column(Integer, primary_key=True)
+
 Class Constructor
 =================
 
@@ -639,10 +791,10 @@ def _as_declarative(cls, classname, dict_):
     # dict_ will be a dictproxy, which we can't write to, and we need to!
     dict_ = dict(dict_)
 
-    column_copies = dict()
-    potential_columns = dict()
+    column_copies = {}
+    potential_columns = {}
     
-    mapper_args ={}
+    mapper_args = {}
     table_args = inherited_table_args = None
     tablename = None
     parent_columns = ()
@@ -664,30 +816,42 @@ def _as_declarative(cls, classname, dict_):
                         if base is not cls:
                             inherited_table_args = True
                 elif base is not cls:
+                    # we're a mixin.
+                    
                     if isinstance(obj, Column):
                         if obj.foreign_keys:
                             raise exceptions.InvalidRequestError(
-                                "Columns with foreign keys to other columns "
-                                "are not allowed on declarative mixins at this time."
-                            )
+                            "Columns with foreign keys to other columns "
+                            "must be declared as @classproperty callables "
+                            "on declarative mixin classes. ")
                         if name not in dict_ and not (
-                                '__table__' in dict_ and name in dict_['__table__'].c
+                                '__table__' in dict_ and 
+                                name in dict_['__table__'].c
                                 ):
-                            potential_columns[name] = column_copies[obj] = obj.copy()
-                            column_copies[obj]._creation_order = obj._creation_order
-                    elif isinstance(obj, RelationshipProperty):
+                            potential_columns[name] = \
+                                    column_copies[obj] = \
+                                    obj.copy()
+                            column_copies[obj]._creation_order = \
+                                    obj._creation_order
+                    elif isinstance(obj, MapperProperty):
                         raise exceptions.InvalidRequestError(
-                                            "relationships are not allowed on "
-                                            "declarative mixins at this time.")
+                            "Mapper properties (i.e. deferred,"
+                            "column_property(), relationship(), etc.) must "
+                            "be declared as @classproperty callables "
+                            "on declarative mixin classes.")
+                    elif isinstance(obj, util.classproperty):
+                        dict_[name] = column_copies[obj] = getattr(cls, name)
+
     # apply inherited columns as we should
     for k, v in potential_columns.items():
         if tablename or k not in parent_columns:
-            dict_[k]=v
+            dict_[k] = v
+            
     if inherited_table_args and not tablename:
         table_args = None
 
-    # make sure that column copies are used rather than the original columns
-    # from any mixins
+    # make sure that column copies are used rather 
+    # than the original columns from any mixins
     for k, v in mapper_args.iteritems():
         mapper_args[k] = column_copies.get(v,v)
     
index 7fc356b67604d4e762ec3f31d9e869677af0bbce..1aafc546914ebf97f6d4d33d5c0b0163a5010e65 100644 (file)
@@ -7,8 +7,10 @@ from sqlalchemy.test import testing
 from sqlalchemy import MetaData, Integer, String, ForeignKey, ForeignKeyConstraint, asc, Index
 from sqlalchemy.test.schema import Table, Column
 from sqlalchemy.orm import relationship, create_session, class_mapper, \
-                            joinedload, compile_mappers, backref, clear_mappers, \
-                            polymorphic_union, deferred
+                            joinedload, compile_mappers, backref, \
+                            clear_mappers, polymorphic_union, \
+                            deferred, column_property
+                            
 from sqlalchemy.test.testing import eq_
 from sqlalchemy.util import classproperty
 
@@ -28,14 +30,16 @@ class DeclarativeTest(DeclarativeTestBase):
         class User(Base, ComparableEntity):
             __tablename__ = 'users'
 
-            id = Column('id', Integer, primary_key=True, test_needs_autoincrement=True)
+            id = Column('id', Integer, primary_key=True,
+                                        test_needs_autoincrement=True)
             name = Column('name', String(50))
             addresses = relationship("Address", backref="user")
 
         class Address(Base, ComparableEntity):
             __tablename__ = 'addresses'
 
-            id = Column(Integer, primary_key=True, test_needs_autoincrement=True)
+            id = Column(Integer, primary_key=True,
+                                        test_needs_autoincrement=True)
             email = Column(String(50), key='_email')
             user_id = Column('user_id', Integer, ForeignKey('users.id'),
                              key='_user_id')
@@ -1656,36 +1660,41 @@ def _produce_test(inline, stringbased):
 
             class User(Base, ComparableEntity):
                 __tablename__ = 'users'
-                id = Column(Integer, primary_key=True, test_needs_autoincrement=True)
+                id = Column(Integer, primary_key=True,
+                                        test_needs_autoincrement=True)
                 name = Column(String(50))
             
             class Address(Base, ComparableEntity):
                 __tablename__ = 'addresses'
-                id = Column(Integer, primary_key=True, test_needs_autoincrement=True)
+                id = Column(Integer, primary_key=True,
+                                        test_needs_autoincrement=True)
                 email = Column(String(50))
                 user_id = Column(Integer, ForeignKey('users.id'))
                 if inline:
                     if stringbased:
                         user = relationship("User", 
-                                                    primaryjoin="User.id==Address.user_id",
-                                                    backref="addresses")
+                                    primaryjoin="User.id==Address.user_id",
+                                    backref="addresses")
                     else:
-                        user = relationship(User, primaryjoin=User.id==user_id, backref="addresses")
+                        user = relationship(User,
+                                    primaryjoin=User.id==user_id,
+                                    backref="addresses")
             
             if not inline:
                 compile_mappers()
                 if stringbased:
                     Address.user = relationship("User", 
-                                            primaryjoin="User.id==Address.user_id",
-                                            backref="addresses")
+                                    primaryjoin="User.id==Address.user_id",
+                                    backref="addresses")
                 else:
                     Address.user = relationship(User, 
-                                            primaryjoin=User.id==Address.user_id,
-                                            backref="addresses")
+                                    primaryjoin=User.id==Address.user_id,
+                                    backref="addresses")
 
         @classmethod
         def insert_data(cls):
-            params = [dict(zip(('id', 'name'), column_values)) for column_values in 
+            params = [dict(zip(('id', 'name'), column_values)) 
+                for column_values in 
                 [(7, 'jack'),
                 (8, 'ed'),
                 (9, 'fred'),
@@ -1694,7 +1703,8 @@ def _produce_test(inline, stringbased):
             User.__table__.insert().execute(params)
         
             Address.__table__.insert().execute(
-                [dict(zip(('id', 'user_id', 'email'), column_values)) for column_values in 
+                [dict(zip(('id', 'user_id', 'email'), column_values)) 
+                for column_values in 
                     [(1, 7, "jack@bean.com"),
                     (2, 8, "ed@wood.com"),
                     (3, 8, "ed@bettyboop.com"),
@@ -1707,8 +1717,10 @@ def _produce_test(inline, stringbased):
             # this query will screw up if the aliasing 
             # enabled in query.join() gets applied to the right half of the 
             # join condition inside the any().
-            # the join condition inside of any() comes from the "primaryjoin" of the relationship,
-            # and should not be annotated with _orm_adapt.  PropertyLoader.Comparator will annotate
+            # the join condition inside of any() comes from the 
+            # "primaryjoin" of the relationship,
+            # and should not be annotated with _orm_adapt. 
+            # PropertyLoader.Comparator will annotate
             # the left side with _orm_adapt, though.
             sess = create_session()
             eq_(
@@ -1878,7 +1890,8 @@ class DeclarativeMixinTest(DeclarativeTestBase):
     def test_simple(self):
 
         class MyMixin(object):
-            id =  Column(Integer, primary_key=True, test_needs_autoincrement=True)
+            id =  Column(Integer, primary_key=True,
+                                test_needs_autoincrement=True)
 
             def foo(self):
                 return 'bar'+str(self.id)
@@ -1899,6 +1912,7 @@ class DeclarativeMixinTest(DeclarativeTestBase):
         eq_(obj.name,'testing')
         eq_(obj.foo(),'bar1')
 
+        
     def test_unique_column(self):
         
         class MyMixin(object):
@@ -1913,7 +1927,8 @@ class DeclarativeMixinTest(DeclarativeTestBase):
     def test_hierarchical_bases(self):
 
         class MyMixinParent:
-            id =  Column(Integer, primary_key=True, test_needs_autoincrement=True)
+            id =  Column(Integer, primary_key=True,
+                                test_needs_autoincrement=True)
 
             def foo(self):
                 return 'bar'+str(self.id)
@@ -1955,6 +1970,19 @@ class DeclarativeMixinTest(DeclarativeTestBase):
                 __tablename__ = 'foo'
         assert_raises(sa.exc.InvalidRequestError, go)
         
+        class MyDefMixin:
+            foo = deferred(Column('foo', String))
+        def go():
+            class MyModel(Base, MyDefMixin):
+                __tablename__ = 'foo'
+        assert_raises(sa.exc.InvalidRequestError, go)
+
+        class MyCPropMixin:
+            foo = column_property(Column('foo', String))
+        def go():
+            class MyModel(Base, MyCPropMixin):
+                __tablename__ = 'foo'
+        assert_raises(sa.exc.InvalidRequestError, go)
         
     def test_table_name_inherited(self):
         
@@ -2069,9 +2097,9 @@ class DeclarativeMixinTest(DeclarativeTestBase):
             @classproperty
             def __mapper_args__(cls):
                 if cls.__name__=='Person':
-                    return dict(polymorphic_on=cls.discriminator)
+                    return {'polymorphic_on':cls.discriminator}
                 else:
-                    return dict(polymorphic_identity=cls.__name__)
+                    return {'polymorphic_identity':cls.__name__}
 
         class Person(Base,ComputedMapperArgs):
             __tablename__ = 'people'
@@ -2095,9 +2123,9 @@ class DeclarativeMixinTest(DeclarativeTestBase):
             @classproperty
             def __mapper_args__(cls):
                 if cls.__name__=='Person':
-                    return dict(polymorphic_on=cls.discriminator)
+                    return {'polymorphic_on':cls.discriminator}
                 else:
-                    return dict(polymorphic_identity=cls.__name__)
+                    return {'polymorphic_identity':cls.__name__}
 
         class Person(Base,ComputedMapperArgs):
             __tablename__ = 'people'
@@ -2141,7 +2169,7 @@ class DeclarativeMixinTest(DeclarativeTestBase):
     def test_mapper_args_inherited(self):
         
         class MyMixin:
-            __mapper_args__=dict(always_refresh=True)
+            __mapper_args__ = {'always_refresh':True}
 
         class MyModel(Base,MyMixin):
             __tablename__='test'
@@ -2171,7 +2199,7 @@ class DeclarativeMixinTest(DeclarativeTestBase):
 
         class MyMixin:
             type_ = Column(String(50))
-            __mapper_args__=dict(polymorphic_on=type_)
+            __mapper_args__= {'polymorphic_on':type_}
 
         class MyModel(Base,MyMixin):
             __tablename__='test'
@@ -2180,8 +2208,7 @@ class DeclarativeMixinTest(DeclarativeTestBase):
         col = MyModel.__mapper__.polymorphic_on
         eq_(col.name,'type_')
         assert col.table is not None
-    
-    
+
     def test_mapper_args_overridden(self):
         
         class MyMixin:
@@ -2198,10 +2225,10 @@ class DeclarativeMixinTest(DeclarativeTestBase):
 
         class MyMixin1:
             type_ = Column(String(50))
-            __mapper_args__=dict(polymorphic_on=type_)
+            __mapper_args__ = {'polymorphic_on':type_}
 
         class MyMixin2:
-            __mapper_args__=dict(always_refresh=True)
+            __mapper_args__ = {'always_refresh':True}
 
         class MyModel(Base,MyMixin1,MyMixin2):
             __tablename__='test'
@@ -2237,7 +2264,8 @@ class DeclarativeMixinTest(DeclarativeTestBase):
 
         assert Specific.__table__ is Generic.__table__
         eq_(Generic.__table__.c.keys(),['id', 'type', 'value'])
-        assert class_mapper(Specific).polymorphic_on is Generic.__table__.c.type
+        assert class_mapper(Specific).polymorphic_on is \
+                    Generic.__table__.c.type
         eq_(class_mapper(Specific).polymorphic_identity, 'specific')
 
     def test_joined_table_propagation(self):
@@ -2291,7 +2319,8 @@ class DeclarativeMixinTest(DeclarativeTestBase):
             id = Column(Integer, ForeignKey('basetype.id'), primary_key=True)
 
         eq_(BaseType.__table__.name,'basetype')
-        eq_(BaseType.__table__.c.keys(),['timestamp', 'type', 'id', 'value', ])
+        eq_(BaseType.__table__.c.keys(),
+                ['timestamp', 'type', 'id', 'value', ])
         eq_(BaseType.__table__.kwargs,{'mysql_engine': 'InnoDB'})
 
         assert Single.__table__ is BaseType.__table__
@@ -2322,7 +2351,8 @@ class DeclarativeMixinTest(DeclarativeTestBase):
         eq_(BaseType.__table__.c.keys(),['type', 'id', 'value'])
 
         assert Specific.__table__ is BaseType.__table__
-        assert class_mapper(Specific).polymorphic_on is BaseType.__table__.c.type
+        assert class_mapper(Specific).polymorphic_on is\
+                        BaseType.__table__.c.type
         eq_(class_mapper(Specific).polymorphic_identity, 'specific')
 
     def test_non_propagating_mixin_used_for_joined(self):
@@ -2439,3 +2469,142 @@ class DeclarativeMixinTest(DeclarativeTestBase):
 
         eq_(Model.__table__.c.keys(),
             ['col1', 'col3', 'col2', 'col4', 'id'])
+
+class DeclarativeMixinPropertyTest(DeclarativeTestBase):
+    def test_column_property(self):
+        class MyMixin(object):
+            @classproperty
+            def prop_hoho(cls):
+                return column_property(Column('prop', String(50)))
+
+        class MyModel(Base,MyMixin):
+            __tablename__ = 'test'
+            id = Column(Integer, primary_key=True,
+                            test_needs_autoincrement=True)
+
+        class MyOtherModel(Base,MyMixin):
+            __tablename__ = 'othertest'
+            id = Column(Integer, primary_key=True,
+                            test_needs_autoincrement=True)
+
+        assert MyModel.__table__.c.prop is not None 
+        assert MyOtherModel.__table__.c.prop is not None 
+        assert MyModel.__table__.c.prop is not MyOtherModel.__table__.c.prop 
+
+        assert MyModel.prop_hoho.property.columns == \
+                        [MyModel.__table__.c.prop] 
+        assert MyOtherModel.prop_hoho.property.columns == \
+                        [MyOtherModel.__table__.c.prop] 
+        assert MyModel.prop_hoho.property is not \
+                    MyOtherModel.prop_hoho.property
+        
+        Base.metadata.create_all()
+        sess = create_session()
+        m1, m2 = MyModel(prop_hoho='foo'), MyOtherModel(prop_hoho='bar')
+        sess.add_all([m1, m2])
+        sess.flush()
+        eq_(
+            sess.query(MyModel).filter(MyModel.prop_hoho=='foo').one(),
+            m1
+        )
+        eq_(
+            sess.query(MyOtherModel).\
+                    filter(MyOtherModel.prop_hoho=='bar').one(),
+            m2
+        )
+        
+    def test_column_in_mapper_args(self):
+        class MyMixin(object):
+            @classproperty
+            def type_(cls):
+                return Column(String(50))
+
+            __mapper_args__= {'polymorphic_on':type_}
+
+        class MyModel(Base,MyMixin):
+            __tablename__='test'
+            id =  Column(Integer, primary_key=True)
+        
+        compile_mappers()
+        col = MyModel.__mapper__.polymorphic_on
+        eq_(col.name,'type_')
+        assert col.table is not None
+    
+    def test_deferred(self):
+        class MyMixin(object):
+            @classproperty
+            def data(cls):
+                return deferred(Column('data', String(50)))
+        
+        class MyModel(Base,MyMixin):
+            __tablename__='test'
+            id =  Column(Integer, primary_key=True,
+                                    test_needs_autoincrement=True)
+        
+        Base.metadata.create_all()
+        sess = create_session()
+        sess.add_all([MyModel(data='d1'), MyModel(data='d2')])
+        sess.flush()
+        sess.expunge_all()
+        
+        d1, d2 = sess.query(MyModel).order_by(MyModel.data)
+        assert 'data' not in d1.__dict__
+        assert d1.data == 'd1'
+        assert 'data' in d1.__dict__
+            
+    def _test_relationship(self, usestring):
+        class RefTargetMixin(object):
+            @classproperty
+            def target_id(cls):
+                return Column('target_id', ForeignKey('target.id'))
+            
+            if usestring:
+                @classproperty
+                def target(cls):
+                    return relationship("Target",
+                        primaryjoin="Target.id==%s.target_id" % cls.__name__
+                    )
+            else:
+                @classproperty
+                def target(cls):
+                    return relationship("Target")
+            
+        class Foo(Base, RefTargetMixin):
+            __tablename__ = 'foo'
+            id = Column(Integer, primary_key=True,
+                            test_needs_autoincrement=True)
+            
+        class Bar(Base, RefTargetMixin):
+            __tablename__ = 'bar'
+            id = Column(Integer, primary_key=True,
+                            test_needs_autoincrement=True)
+                            
+        class Target(Base):
+            __tablename__ = 'target'
+            id = Column(Integer, primary_key=True,
+                            test_needs_autoincrement=True)
+            
+        Base.metadata.create_all()
+        sess = create_session()
+        t1, t2 = Target(), Target()
+        f1, f2, b1 = Foo(target=t1), Foo(target=t2), Bar(target=t1)
+        sess.add_all([f1, f2, b1])
+        sess.flush()
+        
+        eq_(
+            sess.query(Foo).filter(Foo.target==t2).one(),
+            f2
+        )
+        eq_(
+            sess.query(Bar).filter(Bar.target==t2).first(),
+            None
+        )
+        sess.expire_all()
+        eq_(f1.target, t1)
+        
+    def test_relationship(self):
+        self._test_relationship(False)
+
+    def test_relationship_primryjoin(self):
+        self._test_relationship(True)
+        
\ No newline at end of file