From 55ae7d784212c3b03ba74d27e1e088486054c993 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 3 Jul 2010 14:53:37 -0400 Subject: [PATCH] - 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] - un-anglicized the declarative docs --- CHANGES | 9 + doc/build/reference/orm/mapping.rst | 3 + lib/sqlalchemy/ext/declarative.py | 276 ++++++++++++++++++++++------ test/ext/test_declarative.py | 233 +++++++++++++++++++---- 4 files changed, 433 insertions(+), 88 deletions(-) diff --git a/CHANGES b/CHANGES index c7e4251db2..11ae0e5751 100644 --- 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 diff --git a/doc/build/reference/orm/mapping.rst b/doc/build/reference/orm/mapping.rst index b8842d8dc8..8bab92e590 100644 --- a/doc/build/reference/orm/mapping.rst +++ b/doc/build/reference/orm/mapping.rst @@ -91,3 +91,6 @@ Internals .. autoclass:: sqlalchemy.orm.mapper.Mapper :members: + +.. autoclass:: sqlalchemy.orm.interfaces.MapperProperty + :members: diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py index 7ff6051460..d0a02381d3 100755 --- a/lib/sqlalchemy/ext/declarative.py +++ b/lib/sqlalchemy/ext/declarative.py @@ -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) diff --git a/test/ext/test_declarative.py b/test/ext/test_declarative.py index 7fc356b676..1aafc54691 100644 --- a/test/ext/test_declarative.py +++ b/test/ext/test_declarative.py @@ -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 -- 2.47.2