]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Don't hard-evaluate non-ORM @declared_attr for AbstractConcreteBase
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 26 May 2017 17:26:40 +0000 (13:26 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 26 May 2017 17:28:15 +0000 (13:28 -0400)
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
doc/build/changelog/changelog_12.rst
lib/sqlalchemy/ext/declarative/base.py
test/ext/declarative/test_mixin.py

index e7d15d72d952289f3ea0fd7b56de053b204362e1..314fe675c112482676e7b495c64c2741fbca36d5 100644 (file)
         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
index 95f02ea9688f6557add5ddc957795eccecd6a499..d9433e6928c40fe922b7deca69ceb9677a3f7aad 100644 (file)
@@ -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
index 1f9fa1dfaf0c2b43067363c2ff4ba771c7af821b..3272e0753f817dbbd137f4bc624f717d16ddc476 100644 (file)
@@ -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):