summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2017-05-26 13:26:40 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2017-05-26 13:28:15 -0400
commit8b82369347641d1c9d64406462fa5527132c4880 (patch)
treea031040645ba90b6232545f3376419872a8eb00b
parentb1369b47217558779a5b8a17ecd945cedd608dc7 (diff)
downloadsqlalchemy-8b82369347641d1c9d64406462fa5527132c4880.tar.gz
Don't hard-evaluate non-ORM @declared_attr for AbstractConcreteBase
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-Id: I6160ebb3a52c441d6a4b663c8c9bbac6d37fa417 Fixes: #3848
-rw-r--r--doc/build/changelog/changelog_12.rst15
-rw-r--r--lib/sqlalchemy/ext/declarative/base.py6
-rw-r--r--test/ext/declarative/test_mixin.py63
3 files changed, 83 insertions, 1 deletions
diff --git a/doc/build/changelog/changelog_12.rst b/doc/build/changelog/changelog_12.rst
index e7d15d72d..314fe675c 100644
--- a/doc/build/changelog/changelog_12.rst
+++ b/doc/build/changelog/changelog_12.rst
@@ -26,6 +26,21 @@
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
diff --git a/lib/sqlalchemy/ext/declarative/base.py b/lib/sqlalchemy/ext/declarative/base.py
index 95f02ea96..d9433e692 100644
--- a/lib/sqlalchemy/ext/declarative/base.py
+++ b/lib/sqlalchemy/ext/declarative/base.py
@@ -273,6 +273,9 @@ class _MapperConfig(object):
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__'):
@@ -302,7 +305,8 @@ class _MapperConfig(object):
# 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
diff --git a/test/ext/declarative/test_mixin.py b/test/ext/declarative/test_mixin.py
index 1f9fa1dfa..3272e0753 100644
--- a/test/ext/declarative/test_mixin.py
+++ b/test/ext/declarative/test_mixin.py
@@ -1188,6 +1188,69 @@ class DeclarativeMixinTest(DeclarativeTestBase):
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):