and the UPDATE now sets the version_id value to itself, so that
concurrency checks still take place.
+ .. change:: 3848
+ :tags: bug, orm, declarative
+ :tickets: 3848
+
+ Fixed bug where using :class:`.declared_attr` on an
+ :class:`.AbstractConcreteBase` where a particular return value were some
+ non-mapped symbol, including ``None``, would cause the attribute
+ to hard-evaluate just once and store the value to the object
+ dictionary, not allowing it to invoke for subclasses. This behavior
+ is normal when :class:`.declared_attr` is on a mapped class, and
+ does not occur on a mixin or abstract class. Since
+ :class:`.AbstractConcreteBase` is both "abstract" and actually
+ "mapped", a special exception case is made here so that the
+ "abstract" behavior takes precedence for :class:`.declared_attr`.
+
.. change:: 3673
:tags: bug, orm
:tickets: 3673
our_stuff = self.properties
+ late_mapped = _get_immediate_cls_attr(
+ cls, '_sa_decl_prepare_nocascade', strict=True)
+
for k in list(dict_):
if k in ('__table__', '__tablename__', '__mapper_args__'):
# and place the evaluated value onto the class.
if not k.startswith('__'):
dict_.pop(k)
- setattr(cls, k, value)
+ if not late_mapped:
+ setattr(cls, k, value)
continue
# we expect to see the name 'metadata' in some valid cases;
# however at this point we see it's assigned to something trying
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):