Files
sqlalchemy/test/ext/declarative/test_mixin.py
T
Mike Bayer ec1700ba29 Warnings for @declared_attr.cascading
A warning is emitted if a subclass attempts to override an attribute
that was declared on a superclass using ``@declared_attr.cascading``
that the overridden attribute will be ignored. This use
case cannot be fully supported down to further subclasses without more
complex development efforts, so for consistency the "cascading" is
honored all the way down regardless of overriding attributes.

A warning is emitted if the ``@declared_attr.cascading`` attribute is
used with a special declarative name such as ``__tablename__``, as this
has no effect.

Ensure that documenation refers to the current inconsistency that
__tablename__ can be overridden by subclasses however
@declared_attr.cascading cannot.

Fixes: #4091
Fixes: #4092
Change-Id: I3aecdb2f99d408e404a1223f5ad86ae3c7fdf036
2017-09-26 11:36:08 -04:00

1856 lines
55 KiB
Python

from sqlalchemy.testing import eq_, assert_raises, \
assert_raises_message, is_, expect_warnings
from sqlalchemy.ext import declarative as decl
import sqlalchemy as sa
from sqlalchemy import testing
from sqlalchemy import Integer, String, ForeignKey, select, func
from sqlalchemy.testing.schema import Table, Column
from sqlalchemy.orm import relationship, create_session, class_mapper, \
configure_mappers, clear_mappers, \
deferred, column_property, Session, base as orm_base
from sqlalchemy.util import classproperty
from sqlalchemy.ext.declarative import declared_attr, declarative_base
from sqlalchemy.orm import events as orm_events
from sqlalchemy.testing import fixtures, mock
from sqlalchemy.testing.util import gc_collect
Base = None
class DeclarativeTestBase(fixtures.TestBase, testing.AssertsExecutionResults):
def setup(self):
global Base
Base = decl.declarative_base(testing.db)
def teardown(self):
Session.close_all()
clear_mappers()
Base.metadata.drop_all()
class DeclarativeMixinTest(DeclarativeTestBase):
def test_simple(self):
class MyMixin(object):
id = Column(Integer, primary_key=True,
test_needs_autoincrement=True)
def foo(self):
return 'bar' + str(self.id)
class MyModel(Base, MyMixin):
__tablename__ = 'test'
name = Column(String(100), nullable=False, index=True)
Base.metadata.create_all()
session = create_session()
session.add(MyModel(name='testing'))
session.flush()
session.expunge_all()
obj = session.query(MyModel).one()
eq_(obj.id, 1)
eq_(obj.name, 'testing')
eq_(obj.foo(), 'bar1')
def test_unique_column(self):
class MyMixin(object):
id = Column(Integer, primary_key=True)
value = Column(String, unique=True)
class MyModel(Base, MyMixin):
__tablename__ = 'test'
assert MyModel.__table__.c.value.unique
def test_hierarchical_bases(self):
class MyMixinParent:
id = Column(Integer, primary_key=True,
test_needs_autoincrement=True)
def foo(self):
return 'bar' + str(self.id)
class MyMixin(MyMixinParent):
baz = Column(String(100), nullable=False, index=True)
class MyModel(Base, MyMixin):
__tablename__ = 'test'
name = Column(String(100), nullable=False, index=True)
Base.metadata.create_all()
session = create_session()
session.add(MyModel(name='testing', baz='fu'))
session.flush()
session.expunge_all()
obj = session.query(MyModel).one()
eq_(obj.id, 1)
eq_(obj.name, 'testing')
eq_(obj.foo(), 'bar1')
eq_(obj.baz, 'fu')
def test_mixin_overrides(self):
"""test a mixin that overrides a column on a superclass."""
class MixinA(object):
foo = Column(String(50))
class MixinB(MixinA):
foo = Column(Integer)
class MyModelA(Base, MixinA):
__tablename__ = 'testa'
id = Column(Integer, primary_key=True)
class MyModelB(Base, MixinB):
__tablename__ = 'testb'
id = Column(Integer, primary_key=True)
eq_(MyModelA.__table__.c.foo.type.__class__, String)
eq_(MyModelB.__table__.c.foo.type.__class__, Integer)
def test_not_allowed(self):
class MyMixin:
foo = Column(Integer, ForeignKey('bar.id'))
def go():
class MyModel(Base, MyMixin):
__tablename__ = 'foo'
assert_raises(sa.exc.InvalidRequestError, go)
class MyRelMixin:
foo = relationship('Bar')
def go():
class MyModel(Base, MyRelMixin):
__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):
class MyMixin:
@declared_attr
def __tablename__(cls):
return cls.__name__.lower()
id = Column(Integer, primary_key=True)
class MyModel(Base, MyMixin):
pass
eq_(MyModel.__table__.name, 'mymodel')
def test_classproperty_still_works(self):
class MyMixin(object):
@classproperty
def __tablename__(cls):
return cls.__name__.lower()
id = Column(Integer, primary_key=True)
class MyModel(Base, MyMixin):
__tablename__ = 'overridden'
eq_(MyModel.__table__.name, 'overridden')
def test_table_name_not_inherited(self):
class MyMixin:
@declared_attr
def __tablename__(cls):
return cls.__name__.lower()
id = Column(Integer, primary_key=True)
class MyModel(Base, MyMixin):
__tablename__ = 'overridden'
eq_(MyModel.__table__.name, 'overridden')
def test_table_name_inheritance_order(self):
class MyMixin1:
@declared_attr
def __tablename__(cls):
return cls.__name__.lower() + '1'
class MyMixin2:
@declared_attr
def __tablename__(cls):
return cls.__name__.lower() + '2'
class MyModel(Base, MyMixin1, MyMixin2):
id = Column(Integer, primary_key=True)
eq_(MyModel.__table__.name, 'mymodel1')
def test_table_name_dependent_on_subclass(self):
class MyHistoryMixin:
@declared_attr
def __tablename__(cls):
return cls.parent_name + '_changelog'
class MyModel(Base, MyHistoryMixin):
parent_name = 'foo'
id = Column(Integer, primary_key=True)
eq_(MyModel.__table__.name, 'foo_changelog')
def test_table_args_inherited(self):
class MyMixin:
__table_args__ = {'mysql_engine': 'InnoDB'}
class MyModel(Base, MyMixin):
__tablename__ = 'test'
id = Column(Integer, primary_key=True)
eq_(MyModel.__table__.kwargs, {'mysql_engine': 'InnoDB'})
def test_table_args_inherited_descriptor(self):
class MyMixin:
@declared_attr
def __table_args__(cls):
return {'info': cls.__name__}
class MyModel(Base, MyMixin):
__tablename__ = 'test'
id = Column(Integer, primary_key=True)
eq_(MyModel.__table__.info, 'MyModel')
def test_table_args_inherited_single_table_inheritance(self):
class MyMixin:
__table_args__ = {'mysql_engine': 'InnoDB'}
class General(Base, MyMixin):
__tablename__ = 'test'
id = Column(Integer, primary_key=True)
type_ = Column(String(50))
__mapper__args = {'polymorphic_on': type_}
class Specific(General):
__mapper_args__ = {'polymorphic_identity': 'specific'}
assert Specific.__table__ is General.__table__
eq_(General.__table__.kwargs, {'mysql_engine': 'InnoDB'})
def test_columns_single_table_inheritance(self):
"""Test a column on a mixin with an alternate attribute name,
mapped to a superclass and single-table inheritance subclass.
The superclass table gets the column, the subclass shares
the MapperProperty.
"""
class MyMixin(object):
foo = Column('foo', Integer)
bar = Column('bar_newname', Integer)
class General(Base, MyMixin):
__tablename__ = 'test'
id = Column(Integer, primary_key=True)
type_ = Column(String(50))
__mapper__args = {'polymorphic_on': type_}
class Specific(General):
__mapper_args__ = {'polymorphic_identity': 'specific'}
assert General.bar.prop.columns[0] is General.__table__.c.bar_newname
assert len(General.bar.prop.columns) == 1
assert Specific.bar.prop is General.bar.prop
@testing.skip_if(lambda: testing.against('oracle'),
"Test has an empty insert in it at the moment")
def test_columns_single_inheritance_conflict_resolution(self):
"""Test that a declared_attr can return the existing column and it will
be ignored. this allows conditional columns to be added.
See [ticket:2472].
"""
class Person(Base):
__tablename__ = 'person'
id = Column(Integer, primary_key=True)
class Mixin(object):
@declared_attr
def target_id(cls):
return cls.__table__.c.get(
'target_id',
Column(Integer, ForeignKey('other.id'))
)
@declared_attr
def target(cls):
return relationship("Other")
class Engineer(Mixin, Person):
"""single table inheritance"""
class Manager(Mixin, Person):
"""single table inheritance"""
class Other(Base):
__tablename__ = 'other'
id = Column(Integer, primary_key=True)
is_(
Engineer.target_id.property.columns[0],
Person.__table__.c.target_id
)
is_(
Manager.target_id.property.columns[0],
Person.__table__.c.target_id
)
# do a brief round trip on this
Base.metadata.create_all()
session = Session()
o1, o2 = Other(), Other()
session.add_all([
Engineer(target=o1),
Manager(target=o2),
Manager(target=o1)
])
session.commit()
eq_(session.query(Engineer).first().target, o1)
def test_columns_joined_table_inheritance(self):
"""Test a column on a mixin with an alternate attribute name,
mapped to a superclass and joined-table inheritance subclass.
Both tables get the column, in the case of the subclass the two
columns are joined under one MapperProperty.
"""
class MyMixin(object):
foo = Column('foo', Integer)
bar = Column('bar_newname', Integer)
class General(Base, MyMixin):
__tablename__ = 'test'
id = Column(Integer, primary_key=True)
type_ = Column(String(50))
__mapper__args = {'polymorphic_on': type_}
class Specific(General):
__tablename__ = 'sub'
id = Column(Integer, ForeignKey('test.id'), primary_key=True)
__mapper_args__ = {'polymorphic_identity': 'specific'}
assert General.bar.prop.columns[0] is General.__table__.c.bar_newname
assert len(General.bar.prop.columns) == 1
assert Specific.bar.prop is General.bar.prop
eq_(len(Specific.bar.prop.columns), 1)
assert Specific.bar.prop.columns[0] is General.__table__.c.bar_newname
def test_column_join_checks_superclass_type(self):
"""Test that the logic which joins subclass props to those
of the superclass checks that the superclass property is a column.
"""
class General(Base):
__tablename__ = 'test'
id = Column(Integer, primary_key=True)
general_id = Column(Integer, ForeignKey('test.id'))
type_ = relationship("General")
class Specific(General):
__tablename__ = 'sub'
id = Column(Integer, ForeignKey('test.id'), primary_key=True)
type_ = Column('foob', String(50))
assert isinstance(General.type_.property, sa.orm.RelationshipProperty)
assert Specific.type_.property.columns[0] is Specific.__table__.c.foob
def test_column_join_checks_subclass_type(self):
"""Test that the logic which joins subclass props to those
of the superclass checks that the subclass property is a column.
"""
def go():
class General(Base):
__tablename__ = 'test'
id = Column(Integer, primary_key=True)
type_ = Column('foob', Integer)
class Specific(General):
__tablename__ = 'sub'
id = Column(Integer, ForeignKey('test.id'), primary_key=True)
specific_id = Column(Integer, ForeignKey('sub.id'))
type_ = relationship("Specific")
assert_raises_message(
sa.exc.ArgumentError, "column 'foob' conflicts with property", go
)
def test_table_args_overridden(self):
class MyMixin:
__table_args__ = {'mysql_engine': 'Foo'}
class MyModel(Base, MyMixin):
__tablename__ = 'test'
__table_args__ = {'mysql_engine': 'InnoDB'}
id = Column(Integer, primary_key=True)
eq_(MyModel.__table__.kwargs, {'mysql_engine': 'InnoDB'})
@testing.teardown_events(orm_events.MapperEvents)
def test_declare_first_mixin(self):
canary = mock.Mock()
class MyMixin(object):
@classmethod
def __declare_first__(cls):
canary.declare_first__(cls)
@classmethod
def __declare_last__(cls):
canary.declare_last__(cls)
class MyModel(Base, MyMixin):
__tablename__ = 'test'
id = Column(Integer, primary_key=True)
configure_mappers()
eq_(
canary.mock_calls,
[
mock.call.declare_first__(MyModel),
mock.call.declare_last__(MyModel),
]
)
@testing.teardown_events(orm_events.MapperEvents)
def test_declare_first_base(self):
canary = mock.Mock()
class MyMixin(object):
@classmethod
def __declare_first__(cls):
canary.declare_first__(cls)
@classmethod
def __declare_last__(cls):
canary.declare_last__(cls)
class Base(MyMixin):
pass
Base = declarative_base(cls=Base)
class MyModel(Base):
__tablename__ = 'test'
id = Column(Integer, primary_key=True)
configure_mappers()
eq_(
canary.mock_calls,
[
mock.call.declare_first__(MyModel),
mock.call.declare_last__(MyModel),
]
)
@testing.teardown_events(orm_events.MapperEvents)
def test_declare_first_direct(self):
canary = mock.Mock()
class MyOtherModel(Base):
__tablename__ = 'test2'
id = Column(Integer, primary_key=True)
@classmethod
def __declare_first__(cls):
canary.declare_first__(cls)
@classmethod
def __declare_last__(cls):
canary.declare_last__(cls)
configure_mappers()
eq_(
canary.mock_calls,
[
mock.call.declare_first__(MyOtherModel),
mock.call.declare_last__(MyOtherModel)
]
)
def test_mapper_args_declared_attr(self):
class ComputedMapperArgs:
@declared_attr
def __mapper_args__(cls):
if cls.__name__ == 'Person':
return {'polymorphic_on': cls.discriminator}
else:
return {'polymorphic_identity': cls.__name__}
class Person(Base, ComputedMapperArgs):
__tablename__ = 'people'
id = Column(Integer, primary_key=True)
discriminator = Column('type', String(50))
class Engineer(Person):
pass
configure_mappers()
assert class_mapper(Person).polymorphic_on \
is Person.__table__.c.type
eq_(class_mapper(Engineer).polymorphic_identity, 'Engineer')
def test_mapper_args_declared_attr_two(self):
# same as test_mapper_args_declared_attr, but we repeat
# ComputedMapperArgs on both classes for no apparent reason.
class ComputedMapperArgs:
@declared_attr
def __mapper_args__(cls):
if cls.__name__ == 'Person':
return {'polymorphic_on': cls.discriminator}
else:
return {'polymorphic_identity': cls.__name__}
class Person(Base, ComputedMapperArgs):
__tablename__ = 'people'
id = Column(Integer, primary_key=True)
discriminator = Column('type', String(50))
class Engineer(Person, ComputedMapperArgs):
pass
configure_mappers()
assert class_mapper(Person).polymorphic_on \
is Person.__table__.c.type
eq_(class_mapper(Engineer).polymorphic_identity, 'Engineer')
def test_table_args_composite(self):
class MyMixin1:
__table_args__ = {'info': {'baz': 'bob'}}
class MyMixin2:
__table_args__ = {'info': {'foo': 'bar'}}
class MyModel(Base, MyMixin1, MyMixin2):
__tablename__ = 'test'
@declared_attr
def __table_args__(self):
info = {}
args = dict(info=info)
info.update(MyMixin1.__table_args__['info'])
info.update(MyMixin2.__table_args__['info'])
return args
id = Column(Integer, primary_key=True)
eq_(MyModel.__table__.info, {'foo': 'bar', 'baz': 'bob'})
def test_mapper_args_inherited(self):
class MyMixin:
__mapper_args__ = {'always_refresh': True}
class MyModel(Base, MyMixin):
__tablename__ = 'test'
id = Column(Integer, primary_key=True)
eq_(MyModel.__mapper__.always_refresh, True)
def test_mapper_args_inherited_descriptor(self):
class MyMixin:
@declared_attr
def __mapper_args__(cls):
# tenuous, but illustrates the problem!
if cls.__name__ == 'MyModel':
return dict(always_refresh=True)
else:
return dict(always_refresh=False)
class MyModel(Base, MyMixin):
__tablename__ = 'test'
id = Column(Integer, primary_key=True)
eq_(MyModel.__mapper__.always_refresh, True)
def test_mapper_args_polymorphic_on_inherited(self):
class MyMixin:
type_ = Column(String(50))
__mapper_args__ = {'polymorphic_on': type_}
class MyModel(Base, MyMixin):
__tablename__ = 'test'
id = Column(Integer, primary_key=True)
col = MyModel.__mapper__.polymorphic_on
eq_(col.name, 'type_')
assert col.table is not None
def test_mapper_args_overridden(self):
class MyMixin:
__mapper_args__ = dict(always_refresh=True)
class MyModel(Base, MyMixin):
__tablename__ = 'test'
__mapper_args__ = dict(always_refresh=False)
id = Column(Integer, primary_key=True)
eq_(MyModel.__mapper__.always_refresh, False)
def test_mapper_args_composite(self):
class MyMixin1:
type_ = Column(String(50))
__mapper_args__ = {'polymorphic_on': type_}
class MyMixin2:
__mapper_args__ = {'always_refresh': True}
class MyModel(Base, MyMixin1, MyMixin2):
__tablename__ = 'test'
@declared_attr
def __mapper_args__(cls):
args = {}
args.update(MyMixin1.__mapper_args__)
args.update(MyMixin2.__mapper_args__)
if cls.__name__ != 'MyModel':
args.pop('polymorphic_on')
args['polymorphic_identity'] = cls.__name__
return args
id = Column(Integer, primary_key=True)
class MySubModel(MyModel):
pass
eq_(
MyModel.__mapper__.polymorphic_on.name,
'type_'
)
assert MyModel.__mapper__.polymorphic_on.table is not None
eq_(MyModel.__mapper__.always_refresh, True)
eq_(MySubModel.__mapper__.always_refresh, True)
eq_(MySubModel.__mapper__.polymorphic_identity, 'MySubModel')
def test_mapper_args_property(self):
class MyModel(Base):
@declared_attr
def __tablename__(cls):
return cls.__name__.lower()
@declared_attr
def __table_args__(cls):
return {'mysql_engine': 'InnoDB'}
@declared_attr
def __mapper_args__(cls):
args = {}
args['polymorphic_identity'] = cls.__name__
return args
id = Column(Integer, primary_key=True)
class MySubModel(MyModel):
id = Column(Integer, ForeignKey('mymodel.id'), primary_key=True)
class MySubModel2(MyModel):
__tablename__ = 'sometable'
id = Column(Integer, ForeignKey('mymodel.id'), primary_key=True)
eq_(MyModel.__mapper__.polymorphic_identity, 'MyModel')
eq_(MySubModel.__mapper__.polymorphic_identity, 'MySubModel')
eq_(MyModel.__table__.kwargs['mysql_engine'], 'InnoDB')
eq_(MySubModel.__table__.kwargs['mysql_engine'], 'InnoDB')
eq_(MySubModel2.__table__.kwargs['mysql_engine'], 'InnoDB')
eq_(MyModel.__table__.name, 'mymodel')
eq_(MySubModel.__table__.name, 'mysubmodel')
def test_mapper_args_custom_base(self):
"""test the @declared_attr approach from a custom base."""
class Base(object):
@declared_attr
def __tablename__(cls):
return cls.__name__.lower()
@declared_attr
def __table_args__(cls):
return {'mysql_engine': 'InnoDB'}
@declared_attr
def id(self):
return Column(Integer, primary_key=True)
Base = decl.declarative_base(cls=Base)
class MyClass(Base):
pass
class MyOtherClass(Base):
pass
eq_(MyClass.__table__.kwargs['mysql_engine'], 'InnoDB')
eq_(MyClass.__table__.name, 'myclass')
eq_(MyOtherClass.__table__.name, 'myotherclass')
assert MyClass.__table__.c.id.table is MyClass.__table__
assert MyOtherClass.__table__.c.id.table is MyOtherClass.__table__
def test_single_table_no_propagation(self):
class IdColumn:
id = Column(Integer, primary_key=True)
class Generic(Base, IdColumn):
__tablename__ = 'base'
discriminator = Column('type', String(50))
__mapper_args__ = dict(polymorphic_on=discriminator)
value = Column(Integer())
class Specific(Generic):
__mapper_args__ = dict(polymorphic_identity='specific')
assert Specific.__table__ is Generic.__table__
eq_(list(Generic.__table__.c.keys()), ['id', 'type', 'value'])
assert class_mapper(Specific).polymorphic_on \
is Generic.__table__.c.type
eq_(class_mapper(Specific).polymorphic_identity, 'specific')
def test_joined_table_propagation(self):
class CommonMixin:
@declared_attr
def __tablename__(cls):
return cls.__name__.lower()
__table_args__ = {'mysql_engine': 'InnoDB'}
timestamp = Column(Integer)
id = Column(Integer, primary_key=True)
class Generic(Base, CommonMixin):
discriminator = Column('python_type', String(50))
__mapper_args__ = dict(polymorphic_on=discriminator)
class Specific(Generic):
__mapper_args__ = dict(polymorphic_identity='specific')
id = Column(Integer, ForeignKey('generic.id'),
primary_key=True)
eq_(Generic.__table__.name, 'generic')
eq_(Specific.__table__.name, 'specific')
eq_(list(Generic.__table__.c.keys()), ['timestamp', 'id',
'python_type'])
eq_(list(Specific.__table__.c.keys()), ['id'])
eq_(Generic.__table__.kwargs, {'mysql_engine': 'InnoDB'})
eq_(Specific.__table__.kwargs, {'mysql_engine': 'InnoDB'})
def test_some_propagation(self):
class CommonMixin:
@declared_attr
def __tablename__(cls):
return cls.__name__.lower()
__table_args__ = {'mysql_engine': 'InnoDB'}
timestamp = Column(Integer)
class BaseType(Base, CommonMixin):
discriminator = Column('type', String(50))
__mapper_args__ = dict(polymorphic_on=discriminator)
id = Column(Integer, primary_key=True)
value = Column(Integer())
class Single(BaseType):
__tablename__ = None
__mapper_args__ = dict(polymorphic_identity='type1')
class Joined(BaseType):
__mapper_args__ = dict(polymorphic_identity='type2')
id = Column(Integer, ForeignKey('basetype.id'),
primary_key=True)
eq_(BaseType.__table__.name, 'basetype')
eq_(list(BaseType.__table__.c.keys()), ['timestamp', 'type', 'id',
'value'])
eq_(BaseType.__table__.kwargs, {'mysql_engine': 'InnoDB'})
assert Single.__table__ is BaseType.__table__
eq_(Joined.__table__.name, 'joined')
eq_(list(Joined.__table__.c.keys()), ['id'])
eq_(Joined.__table__.kwargs, {'mysql_engine': 'InnoDB'})
def test_col_copy_vs_declared_attr_joined_propagation(self):
class Mixin(object):
a = Column(Integer)
@declared_attr
def b(cls):
return Column(Integer)
class A(Mixin, Base):
__tablename__ = 'a'
id = Column(Integer, primary_key=True)
class B(A):
__tablename__ = 'b'
id = Column(Integer, ForeignKey('a.id'), primary_key=True)
assert 'a' in A.__table__.c
assert 'b' in A.__table__.c
assert 'a' not in B.__table__.c
assert 'b' not in B.__table__.c
def test_col_copy_vs_declared_attr_joined_propagation_newname(self):
class Mixin(object):
a = Column('a1', Integer)
@declared_attr
def b(cls):
return Column('b1', Integer)
class A(Mixin, Base):
__tablename__ = 'a'
id = Column(Integer, primary_key=True)
class B(A):
__tablename__ = 'b'
id = Column(Integer, ForeignKey('a.id'), primary_key=True)
assert 'a1' in A.__table__.c
assert 'b1' in A.__table__.c
assert 'a1' not in B.__table__.c
assert 'b1' not in B.__table__.c
def test_col_copy_vs_declared_attr_single_propagation(self):
class Mixin(object):
a = Column(Integer)
@declared_attr
def b(cls):
return Column(Integer)
class A(Mixin, Base):
__tablename__ = 'a'
id = Column(Integer, primary_key=True)
class B(A):
pass
assert 'a' in A.__table__.c
assert 'b' in A.__table__.c
def test_non_propagating_mixin(self):
class NoJoinedTableNameMixin:
@declared_attr
def __tablename__(cls):
if decl.has_inherited_table(cls):
return None
return cls.__name__.lower()
class BaseType(Base, NoJoinedTableNameMixin):
discriminator = Column('type', String(50))
__mapper_args__ = dict(polymorphic_on=discriminator)
id = Column(Integer, primary_key=True)
value = Column(Integer())
class Specific(BaseType):
__mapper_args__ = dict(polymorphic_identity='specific')
eq_(BaseType.__table__.name, 'basetype')
eq_(list(BaseType.__table__.c.keys()), ['type', 'id', 'value'])
assert Specific.__table__ is BaseType.__table__
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):
class TableNameMixin:
@declared_attr
def __tablename__(cls):
if decl.has_inherited_table(cls) and TableNameMixin \
not in cls.__bases__:
return None
return cls.__name__.lower()
class BaseType(Base, TableNameMixin):
discriminator = Column('type', String(50))
__mapper_args__ = dict(polymorphic_on=discriminator)
id = Column(Integer, primary_key=True)
value = Column(Integer())
class Specific(BaseType, TableNameMixin):
__mapper_args__ = dict(polymorphic_identity='specific')
id = Column(Integer, ForeignKey('basetype.id'),
primary_key=True)
eq_(BaseType.__table__.name, 'basetype')
eq_(list(BaseType.__table__.c.keys()), ['type', 'id', 'value'])
eq_(Specific.__table__.name, 'specific')
eq_(list(Specific.__table__.c.keys()), ['id'])
def test_single_back_propagate(self):
class ColumnMixin:
timestamp = Column(Integer)
class BaseType(Base):
__tablename__ = 'foo'
discriminator = Column('type', String(50))
__mapper_args__ = dict(polymorphic_on=discriminator)
id = Column(Integer, primary_key=True)
class Specific(BaseType, ColumnMixin):
__mapper_args__ = dict(polymorphic_identity='specific')
eq_(list(BaseType.__table__.c.keys()), ['type', 'id', 'timestamp'])
def test_table_in_model_and_same_column_in_mixin(self):
class ColumnMixin:
data = Column(Integer)
class Model(Base, ColumnMixin):
__table__ = Table('foo', Base.metadata,
Column('data', Integer),
Column('id', Integer, primary_key=True))
model_col = Model.__table__.c.data
mixin_col = ColumnMixin.data
assert model_col is not mixin_col
eq_(model_col.name, 'data')
assert model_col.type.__class__ is mixin_col.type.__class__
def test_table_in_model_and_different_named_column_in_mixin(self):
class ColumnMixin:
tada = Column(Integer)
def go():
class Model(Base, ColumnMixin):
__table__ = Table('foo', Base.metadata,
Column('data', Integer),
Column('id', Integer, primary_key=True))
foo = relationship("Dest")
assert_raises_message(sa.exc.ArgumentError,
"Can't add additional column 'tada' when "
"specifying __table__", go)
def test_table_in_model_and_different_named_alt_key_column_in_mixin(self):
# here, the __table__ has a column 'tada'. We disallow
# the add of the 'foobar' column, even though it's
# keyed to 'tada'.
class ColumnMixin:
tada = Column('foobar', Integer)
def go():
class Model(Base, ColumnMixin):
__table__ = Table('foo', Base.metadata,
Column('data', Integer),
Column('tada', Integer),
Column('id', Integer, primary_key=True))
foo = relationship("Dest")
assert_raises_message(sa.exc.ArgumentError,
"Can't add additional column 'foobar' when "
"specifying __table__", go)
def test_table_in_model_overrides_different_typed_column_in_mixin(self):
class ColumnMixin:
data = Column(String)
class Model(Base, ColumnMixin):
__table__ = Table('foo', Base.metadata,
Column('data', Integer),
Column('id', Integer, primary_key=True))
model_col = Model.__table__.c.data
mixin_col = ColumnMixin.data
assert model_col is not mixin_col
eq_(model_col.name, 'data')
assert model_col.type.__class__ is Integer
def test_mixin_column_ordering(self):
class Foo(object):
col1 = Column(Integer)
col3 = Column(Integer)
class Bar(object):
col2 = Column(Integer)
col4 = Column(Integer)
class Model(Base, Foo, Bar):
id = Column(Integer, primary_key=True)
__tablename__ = 'model'
eq_(list(Model.__table__.c.keys()), ['col1', 'col3', 'col2', 'col4',
'id'])
def test_honor_class_mro_one(self):
class HasXMixin(object):
@declared_attr
def x(self):
return Column(Integer)
class Parent(HasXMixin, Base):
__tablename__ = 'parent'
id = Column(Integer, primary_key=True)
class Child(Parent):
__tablename__ = 'child'
id = Column(Integer, ForeignKey('parent.id'), primary_key=True)
assert "x" not in Child.__table__.c
def test_honor_class_mro_two(self):
class HasXMixin(object):
@declared_attr
def x(self):
return Column(Integer)
class Parent(HasXMixin, Base):
__tablename__ = 'parent'
id = Column(Integer, primary_key=True)
def x(self):
return "hi"
class C(Parent):
__tablename__ = 'c'
id = Column(Integer, ForeignKey('parent.id'), primary_key=True)
assert C().x() == 'hi'
def test_arbitrary_attrs_one(self):
class HasMixin(object):
@declared_attr
def some_attr(cls):
return cls.__name__ + "SOME ATTR"
class Mapped(HasMixin, Base):
__tablename__ = 't'
id = Column(Integer, primary_key=True)
eq_(Mapped.some_attr, "MappedSOME ATTR")
eq_(Mapped.__dict__['some_attr'], "MappedSOME ATTR")
def test_arbitrary_attrs_two(self):
from sqlalchemy.ext.associationproxy import association_proxy
class FilterA(Base):
__tablename__ = 'filter_a'
id = Column(Integer(), primary_key=True)
parent_id = Column(Integer(),
ForeignKey('type_a.id'))
filter = Column(String())
def __init__(self, filter_, **kw):
self.filter = filter_
class FilterB(Base):
__tablename__ = 'filter_b'
id = Column(Integer(), primary_key=True)
parent_id = Column(Integer(),
ForeignKey('type_b.id'))
filter = Column(String())
def __init__(self, filter_, **kw):
self.filter = filter_
class FilterMixin(object):
@declared_attr
def _filters(cls):
return relationship(cls.filter_class,
cascade='all,delete,delete-orphan')
@declared_attr
def filters(cls):
return association_proxy('_filters', 'filter')
class TypeA(Base, FilterMixin):
__tablename__ = 'type_a'
filter_class = FilterA
id = Column(Integer(), primary_key=True)
class TypeB(Base, FilterMixin):
__tablename__ = 'type_b'
filter_class = FilterB
id = Column(Integer(), primary_key=True)
TypeA(filters=['foo'])
TypeB(filters=['foo'])
def test_arbitrary_attrs_three(self):
class Mapped(Base):
__tablename__ = 't'
id = Column(Integer, primary_key=True)
@declared_attr
def some_attr(cls):
return cls.__name__ + "SOME ATTR"
eq_(Mapped.some_attr, "MappedSOME ATTR")
eq_(Mapped.__dict__['some_attr'], "MappedSOME ATTR")
def test_arbitrary_attrs_doesnt_apply_to_abstract_declared_attr(self):
names = ["name1", "name2", "name3"]
class SomeAbstract(Base):
__abstract__ = True
@declared_attr
def some_attr(cls):
return names.pop(0)
class M1(SomeAbstract):
__tablename__ = 't1'
id = Column(Integer, primary_key=True)
class M2(SomeAbstract):
__tablename__ = 't2'
id = Column(Integer, primary_key=True)
eq_(M1.__dict__['some_attr'], 'name1')
eq_(M2.__dict__['some_attr'], 'name2')
def test_arbitrary_attrs_doesnt_apply_to_prepare_nocascade(self):
names = ["name1", "name2", "name3"]
class SomeAbstract(Base):
__tablename__ = 't0'
__no_table__ = True
# used by AbstractConcreteBase
_sa_decl_prepare_nocascade = True
id = Column(Integer, primary_key=True)
@declared_attr
def some_attr(cls):
return names.pop(0)
class M1(SomeAbstract):
__tablename__ = 't1'
id = Column(Integer, primary_key=True)
class M2(SomeAbstract):
__tablename__ = 't2'
id = Column(Integer, primary_key=True)
eq_(M1.some_attr, "name2")
eq_(M2.some_attr, "name3")
eq_(M1.__dict__['some_attr'], 'name2')
eq_(M2.__dict__['some_attr'], 'name3')
assert isinstance(SomeAbstract.__dict__['some_attr'], declared_attr)
class DeclarativeMixinPropertyTest(DeclarativeTestBase):
def test_column_property(self):
class MyMixin(object):
@declared_attr
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_doc(self):
"""test documentation transfer.
the documentation situation with @declared_attr is problematic.
at least see if mapped subclasses get the doc.
"""
class MyMixin(object):
@declared_attr
def type_(cls):
"""this is a document."""
return Column(String(50))
@declared_attr
def t2(cls):
"""this is another document."""
return column_property(Column(String(50)))
class MyModel(Base, MyMixin):
__tablename__ = 'test'
id = Column(Integer, primary_key=True)
configure_mappers()
eq_(MyModel.type_.__doc__, """this is a document.""")
eq_(MyModel.t2.__doc__, """this is another document.""")
def test_column_in_mapper_args(self):
class MyMixin(object):
@declared_attr
def type_(cls):
return Column(String(50))
__mapper_args__ = {'polymorphic_on': type_}
class MyModel(Base, MyMixin):
__tablename__ = 'test'
id = Column(Integer, primary_key=True)
configure_mappers()
col = MyModel.__mapper__.polymorphic_on
eq_(col.name, 'type_')
assert col.table is not None
def test_column_in_mapper_args_used_multiple_times(self):
class MyMixin(object):
version_id = Column(Integer)
__mapper_args__ = {'version_id_col': version_id}
class ModelOne(Base, MyMixin):
__tablename__ = 'm1'
id = Column(Integer, primary_key=True)
class ModelTwo(Base, MyMixin):
__tablename__ = 'm2'
id = Column(Integer, primary_key=True)
is_(
ModelOne.__mapper__.version_id_col,
ModelOne.__table__.c.version_id
)
is_(
ModelTwo.__mapper__.version_id_col,
ModelTwo.__table__.c.version_id
)
def test_deferred(self):
class MyMixin(object):
@declared_attr
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):
@declared_attr
def target_id(cls):
return Column('target_id', ForeignKey('target.id'))
if usestring:
@declared_attr
def target(cls):
return relationship('Target',
primaryjoin='Target.id==%s.target_id'
% cls.__name__)
else:
@declared_attr
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)
class DeclaredAttrTest(DeclarativeTestBase, testing.AssertsCompiledSQL):
__dialect__ = 'default'
def test_singleton_behavior_within_decl(self):
counter = mock.Mock()
class Mixin(object):
@declared_attr
def my_prop(cls):
counter(cls)
return Column('x', Integer)
class A(Base, Mixin):
__tablename__ = 'a'
id = Column(Integer, primary_key=True)
@declared_attr
def my_other_prop(cls):
return column_property(cls.my_prop + 5)
eq_(counter.mock_calls, [mock.call(A)])
class B(Base, Mixin):
__tablename__ = 'b'
id = Column(Integer, primary_key=True)
@declared_attr
def my_other_prop(cls):
return column_property(cls.my_prop + 5)
eq_(
counter.mock_calls,
[mock.call(A), mock.call(B)])
# this is why we need singleton-per-class behavior. We get
# an un-bound "x" column otherwise here, because my_prop() generates
# multiple columns.
a_col = A.my_other_prop.__clause_element__().element.left
b_col = B.my_other_prop.__clause_element__().element.left
is_(a_col.table, A.__table__)
is_(b_col.table, B.__table__)
is_(a_col, A.__table__.c.x)
is_(b_col, B.__table__.c.x)
s = Session()
self.assert_compile(
s.query(A),
"SELECT a.x AS a_x, a.x + :x_1 AS anon_1, a.id AS a_id FROM a"
)
self.assert_compile(
s.query(B),
"SELECT b.x AS b_x, b.x + :x_1 AS anon_1, b.id AS b_id FROM b"
)
@testing.requires.predictable_gc
def test_singleton_gc(self):
counter = mock.Mock()
class Mixin(object):
@declared_attr
def my_prop(cls):
counter(cls.__name__)
return Column('x', Integer)
class A(Base, Mixin):
__tablename__ = 'b'
id = Column(Integer, primary_key=True)
@declared_attr
def my_other_prop(cls):
return column_property(cls.my_prop + 5)
eq_(counter.mock_calls, [mock.call("A")])
del A
gc_collect()
assert "A" not in Base._decl_class_registry
def test_can_we_access_the_mixin_straight(self):
class Mixin(object):
@declared_attr
def my_prop(cls):
return Column('x', Integer)
assert_raises_message(
sa.exc.SAWarning,
"Unmanaged access of declarative attribute my_prop "
"from non-mapped class Mixin",
getattr, Mixin, "my_prop"
)
def test_non_decl_access(self):
counter = mock.Mock()
class Mixin(object):
@declared_attr
def __tablename__(cls):
counter(cls)
return "foo"
class Foo(Mixin, Base):
id = Column(Integer, primary_key=True)
@declared_attr
def x(cls):
cls.__tablename__
@declared_attr
def y(cls):
cls.__tablename__
eq_(
counter.mock_calls,
[mock.call(Foo)]
)
eq_(Foo.__tablename__, 'foo')
eq_(Foo.__tablename__, 'foo')
eq_(
counter.mock_calls,
[mock.call(Foo), mock.call(Foo), mock.call(Foo)]
)
def test_property_noncascade(self):
counter = mock.Mock()
class Mixin(object):
@declared_attr
def my_prop(cls):
counter(cls)
return column_property(cls.x + 2)
class A(Base, Mixin):
__tablename__ = 'a'
id = Column(Integer, primary_key=True)
x = Column(Integer)
class B(A):
pass
eq_(counter.mock_calls, [mock.call(A)])
def test_property_cascade_mixin(self):
counter = mock.Mock()
class Mixin(object):
@declared_attr.cascading
def my_prop(cls):
counter(cls)
return column_property(cls.x + 2)
class A(Base, Mixin):
__tablename__ = 'a'
id = Column(Integer, primary_key=True)
x = Column(Integer)
class B(A):
pass
eq_(counter.mock_calls, [mock.call(A), mock.call(B)])
def test_property_cascade_mixin_override(self):
counter = mock.Mock()
class Mixin(object):
@declared_attr.cascading
def my_prop(cls):
counter(cls)
return column_property(cls.x + 2)
class A(Base, Mixin):
__tablename__ = 'a'
id = Column(Integer, primary_key=True)
x = Column(Integer)
with expect_warnings(
"Attribute 'my_prop' on class .*B.* "
"cannot be processed due to @declared_attr.cascading; "
"skipping"):
class B(A):
my_prop = Column('foobar', Integer)
eq_(counter.mock_calls, [mock.call(A), mock.call(B)])
def test_property_cascade_abstract(self):
counter = mock.Mock()
class Abs(Base):
__abstract__ = True
@declared_attr.cascading
def my_prop(cls):
counter(cls)
return column_property(cls.x + 2)
class A(Abs):
__tablename__ = 'a'
id = Column(Integer, primary_key=True)
x = Column(Integer)
class B(A):
pass
eq_(counter.mock_calls, [mock.call(A), mock.call(B)])
def test_warn_cascading_used_w_tablename(self):
class Mixin(object):
@declared_attr.cascading
def __tablename__(cls):
return "foo"
with expect_warnings(
"@declared_attr.cascading is not supported on the "
"__tablename__ attribute on class .*A."
):
class A(Mixin, Base):
id = Column(Integer, primary_key=True)
eq_(A.__table__.name, "foo")
def test_col_prop_attrs_associated_w_class_for_mapper_args(self):
from sqlalchemy import Column
import collections
asserted = collections.defaultdict(set)
class Mixin(object):
@declared_attr.cascading
def my_attr(cls):
if decl.has_inherited_table(cls):
id = Column(ForeignKey('a.my_attr'), primary_key=True)
asserted['b'].add(id)
else:
id = Column(Integer, primary_key=True)
asserted['a'].add(id)
return id
class A(Base, Mixin):
__tablename__ = 'a'
@declared_attr
def __mapper_args__(cls):
asserted['a'].add(cls.my_attr)
return {}
# here:
# 1. A is mapped. so A.my_attr is now the InstrumentedAttribute.
# 2. B wants to call my_attr also. Due to .cascading, it has been
# invoked specific to B, and is present in the dict_ that will
# be used when we map the class. But except for the
# special setattr() we do in _scan_attributes() in this case, would
# otherwise not been set on the class as anything from this call;
# the usual mechanics of calling it from the descriptor also do not
# work because A is fully mapped and because A set it up, is currently
# that non-expected InstrumentedAttribute and replaces the
# descriptor from being invoked.
class B(A):
__tablename__ = 'b'
@declared_attr
def __mapper_args__(cls):
asserted['b'].add(cls.my_attr)
return {}
eq_(
asserted,
{
'a': set([A.my_attr.property.columns[0]]),
'b': set([B.my_attr.property.columns[0]])
}
)
def test_column_pre_map(self):
counter = mock.Mock()
class Mixin(object):
@declared_attr
def my_col(cls):
counter(cls)
assert not orm_base._mapper_or_none(cls)
return Column('x', Integer)
class A(Base, Mixin):
__tablename__ = 'a'
id = Column(Integer, primary_key=True)
eq_(counter.mock_calls, [mock.call(A)])
def test_mixin_attr_refers_to_column_copies(self):
# this @declared_attr can refer to User.id
# freely because we now do the "copy column" operation
# before the declared_attr is invoked.
counter = mock.Mock()
class HasAddressCount(object):
id = Column(Integer, primary_key=True)
@declared_attr
def address_count(cls):
counter(cls.id)
return column_property(
select([func.count(Address.id)]).
where(Address.user_id == cls.id).
as_scalar()
)
class Address(Base):
__tablename__ = 'address'
id = Column(Integer, primary_key=True)
user_id = Column(ForeignKey('user.id'))
class User(Base, HasAddressCount):
__tablename__ = 'user'
eq_(
counter.mock_calls,
[mock.call(User.id)]
)
sess = Session()
self.assert_compile(
sess.query(User).having(User.address_count > 5),
'SELECT (SELECT count(address.id) AS '
'count_1 FROM address WHERE address.user_id = "user".id) '
'AS anon_1, "user".id AS user_id FROM "user" '
'HAVING (SELECT count(address.id) AS '
'count_1 FROM address WHERE address.user_id = "user".id) '
'> :param_1'
)
class AbstractTest(DeclarativeTestBase):
def test_abstract_boolean(self):
class A(Base):
__abstract__ = True
__tablename__ = 'x'
id = Column(Integer, primary_key=True)
class B(Base):
__abstract__ = False
__tablename__ = 'y'
id = Column(Integer, primary_key=True)
class C(Base):
__abstract__ = False
__tablename__ = 'z'
id = Column(Integer, primary_key=True)
class D(Base):
__tablename__ = 'q'
id = Column(Integer, primary_key=True)
eq_(set(Base.metadata.tables), set(['y', 'z', 'q']))
def test_middle_abstract_attributes(self):
# test for [ticket:3219]
class A(Base):
__tablename__ = 'a'
id = Column(Integer, primary_key=True)
name = Column(String)
class B(A):
__abstract__ = True
data = Column(String)
class C(B):
c_value = Column(String)
eq_(
sa.inspect(C).attrs.keys(), ['id', 'name', 'data', 'c_value']
)
def test_middle_abstract_inherits(self):
# test for [ticket:3240]
class A(Base):
__tablename__ = 'a'
id = Column(Integer, primary_key=True)
class AAbs(A):
__abstract__ = True
class B1(A):
__tablename__ = 'b1'
id = Column(ForeignKey('a.id'), primary_key=True)
class B2(AAbs):
__tablename__ = 'b2'
id = Column(ForeignKey('a.id'), primary_key=True)
assert B1.__mapper__.inherits is A.__mapper__
assert B2.__mapper__.inherits is A.__mapper__