]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Fixed regression regarding the declarative ``__declare_first__``
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 24 Apr 2015 17:49:09 +0000 (13:49 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 24 Apr 2015 17:49:09 +0000 (13:49 -0400)
and ``__declare_last__`` accessors where these would no longer be
called on the superclass of the declarative base.
fixes #3383

doc/build/changelog/changelog_10.rst
lib/sqlalchemy/__init__.py
lib/sqlalchemy/ext/declarative/base.py
lib/sqlalchemy/testing/__init__.py
lib/sqlalchemy/testing/util.py
test/ext/declarative/test_inheritance.py
test/ext/declarative/test_mixin.py

index 29e686391098ed2fbbfba8cbab247e8345c8d14b..319e58b78cfd1373c6c54196e2121066a23308df 100644 (file)
     .. include:: changelog_07.rst
         :start-line: 5
 
+.. changelog::
+    :version: 1.0.2
+
+    .. change::
+        :tags: bug, ext, declarative
+        :tickets: 3383
+
+        Fixed regression regarding the declarative ``__declare_first__``
+        and ``__declare_last__`` accessors where these would no longer be
+        called on the superclass of the declarative base.
+
 .. changelog::
     :version: 1.0.1
     :released: April 23, 2015
index 3ef97ef90f42300709cca67e7fc3eeeaaa160388..69ab887726cb1f7204901ca1d80f7d1e8d501776 100644 (file)
@@ -120,7 +120,7 @@ from .schema import (
 from .inspection import inspect
 from .engine import create_engine, engine_from_config
 
-__version__ = '1.0.1'
+__version__ = '1.0.2'
 
 
 def __go(lcls):
index 7d4020b240bd514a54b91a09fc548103f67b2cef..d5fc76ed14ab54e7299816f8cbfafb78172842bf 100644 (file)
@@ -50,7 +50,7 @@ def _resolve_for_abstract(cls):
         return cls
 
 
-def _get_immediate_cls_attr(cls, attrname):
+def _get_immediate_cls_attr(cls, attrname, strict=False):
     """return an attribute of the class that is either present directly
     on the class, e.g. not on a superclass, or is from a superclass but
     this superclass is a mixin, that is, not a descendant of
@@ -66,11 +66,12 @@ def _get_immediate_cls_attr(cls, attrname):
 
     for base in cls.__mro__:
         _is_declarative_inherits = hasattr(base, '_decl_class_registry')
-        if attrname in base.__dict__:
-            value = getattr(base, attrname)
-            if (base is cls or
-                    (base in cls.__bases__ and not _is_declarative_inherits)):
-                return value
+        if attrname in base.__dict__ and (
+            base is cls or
+            ((base in cls.__bases__ if strict else True)
+                and not _is_declarative_inherits)
+        ):
+            return getattr(base, attrname)
     else:
         return None
 
@@ -92,7 +93,7 @@ class _MapperConfig(object):
     @classmethod
     def setup_mapping(cls, cls_, classname, dict_):
         defer_map = _get_immediate_cls_attr(
-            cls_, '_sa_decl_prepare_nocascade') or \
+            cls_, '_sa_decl_prepare_nocascade', strict=True) or \
             hasattr(cls_, '_sa_decl_prepare')
 
         if defer_map:
@@ -158,7 +159,8 @@ class _MapperConfig(object):
         for base in cls.__mro__:
             class_mapped = base is not cls and \
                 _declared_mapping_info(base) is not None and \
-                not _get_immediate_cls_attr(base, '_sa_decl_prepare_nocascade')
+                not _get_immediate_cls_attr(
+                    base, '_sa_decl_prepare_nocascade', strict=True)
 
             if not class_mapped and base is not cls:
                 self._produce_column_copies(base)
@@ -412,7 +414,7 @@ class _MapperConfig(object):
                 continue
             if _declared_mapping_info(c) is not None and \
                     not _get_immediate_cls_attr(
-                        c, '_sa_decl_prepare_nocascade'):
+                        c, '_sa_decl_prepare_nocascade', strict=True):
                 self.inherits = c
                 break
         else:
index bf83e9673bc622ff0bb4bcaaf2de934be21a5325..adfbe85e301f21869d5501a2ce44fa42c047f2be 100644 (file)
@@ -24,7 +24,8 @@ from .assertions import emits_warning, emits_warning_on, uses_deprecated, \
     AssertsExecutionResults, expect_deprecated, expect_warnings
 
 from .util import run_as_contextmanager, rowset, fail, \
-    provide_metadata, adict, force_drop_names
+    provide_metadata, adict, force_drop_names, \
+    teardown_events
 
 crashes = skip
 
index 1842e58a5ce00834196b4f98b2037cfea6710297..e9437948a3e55e73b21f2b3ddfe2d47ea88a3a09 100644 (file)
@@ -267,3 +267,14 @@ def drop_all_tables(engine, inspector, schema=None, include_names=None):
                         ForeignKeyConstraint(
                             [tb.c.x], [tb.c.y], name=fkc)
                     ))
+
+
+def teardown_events(event_cls):
+    @decorator
+    def decorate(fn, *arg, **kw):
+        try:
+            return fn(*arg, **kw)
+        finally:
+            event_cls._clear()
+    return decorate
+
index 2ecee99fdd39eab34a2c4b3ac91b29d037cf0a31..3e6980190f63b90f3ffbb6ad35021948911a4d7e 100644 (file)
@@ -1451,4 +1451,5 @@ class ConcreteExtensionConfigTest(
             "actual_documents.send_method AS send_method, "
             "actual_documents.id AS id, 'actual' AS type "
             "FROM actual_documents) AS pjoin"
-        )
\ No newline at end of file
+        )
+
index 45b881671064268a62c80172add6dd6995cba4b9..b9e40421c77f69a8b9355e682eef93b530b82c1f 100644 (file)
@@ -9,7 +9,8 @@ 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
+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
 
@@ -438,6 +439,90 @@ class DeclarativeMixinTest(DeclarativeTestBase):
 
         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: