]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Fixed a regression regarding the :meth:`.MapperEvents.instrument_class`
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 26 Apr 2015 22:22:41 +0000 (18:22 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 26 Apr 2015 22:22:41 +0000 (18:22 -0400)
event where its invocation was moved to be after the class manager's
instrumentation of the class, which is the opposite of what the
documentation for the event explicitly states.  The rationale for the
switch was due to Declarative taking the step of setting up
the full "instrumentation manager" for a class before it was mapped
for the purpose of the new ``@declared_attr`` features
described in :ref:`feature_3150`, but the change was also made
against the classical use of :func:`.mapper` for consistency.
However, SQLSoup relies upon the instrumentation event happening
before any instrumentation under classical mapping.
The behavior is reverted in the case of classical and declarative
mapping, the latter implemented by using a simple memoization
without using class manager.
fixes #3388

doc/build/changelog/changelog_10.rst
lib/sqlalchemy/ext/declarative/api.py
lib/sqlalchemy/ext/declarative/base.py
lib/sqlalchemy/orm/mapper.py
test/ext/declarative/test_basic.py
test/orm/test_events.py

index 5b410e74ff854a2283cd0d94407812571a5d52e2..891981fe2777e3c2a555e596ab7b9445df8aadc0 100644 (file)
 .. changelog::
     :version: 1.0.3
 
+    .. change::
+        :tags: bug, orm
+        :tickets: 3388
+
+        Fixed a regression regarding the :meth:`.MapperEvents.instrument_class`
+        event where its invocation was moved to be after the class manager's
+        instrumentation of the class, which is the opposite of what the
+        documentation for the event explicitly states.  The rationale for the
+        switch was due to Declarative taking the step of setting up
+        the full "instrumentation manager" for a class before it was mapped
+        for the purpose of the new ``@declared_attr`` features
+        described in :ref:`feature_3150`, but the change was also made
+        against the classical use of :func:`.mapper` for consistency.
+        However, SQLSoup relies upon the instrumentation event happening
+        before any instrumentation under classical mapping.
+        The behavior is reverted in the case of classical and declarative
+        mapping, the latter implemented by using a simple memoization
+        without using class manager.
+
     .. change::
         :tags: bug, orm
         :tickets: 3387
index 713ea0aba3ac173d74a8fd31de7bd21998239a65..3d46bd4cb291b08c164673f04cc72fc3f7981962 100644 (file)
@@ -163,21 +163,16 @@ class declared_attr(interfaces._MappedAttribute, property):
         self._cascading = cascading
 
     def __get__(desc, self, cls):
-        # use the ClassManager for memoization of values.  This is better than
-        # adding yet another attribute onto the class, or using weakrefs
-        # here which are slow and take up memory.  It also allows us to
-        # warn for non-mapped use of declared_attr.
-
-        manager = attributes.manager_of_class(cls)
-        if manager is None:
-            util.warn(
-                "Unmanaged access of declarative attribute %s from "
-                "non-mapped class %s" %
-                (desc.fget.__name__, cls.__name__))
+        reg = cls.__dict__.get('_sa_declared_attr_reg', None)
+        if reg is None:
+            manager = attributes.manager_of_class(cls)
+            if manager is None:
+                util.warn(
+                    "Unmanaged access of declarative attribute %s from "
+                    "non-mapped class %s" %
+                    (desc.fget.__name__, cls.__name__))
             return desc.fget(cls)
 
-        reg = manager.info.get('declared_attr_reg', None)
-
         if reg is None:
             return desc.fget(cls)
         elif desc in reg:
index 062936ea7fe3445fcce56b741b74d12db92b1ca8..57eb54f63e773faf886efe9417f8b823e03f368d 100644 (file)
@@ -115,10 +115,10 @@ class _MapperConfig(object):
         self.column_copies = {}
         self._setup_declared_events()
 
-        # register up front, so that @declared_attr can memoize
-        # function evaluations in .info
-        manager = instrumentation.register_class(self.cls)
-        manager.info['declared_attr_reg'] = {}
+        # temporary registry.  While early 1.0 versions
+        # set up the ClassManager here, by API contract
+        # we can't do that until there's a mapper.
+        self.cls._sa_declared_attr_reg = {}
 
         self._scan_attributes()
 
@@ -529,7 +529,7 @@ class _MapperConfig(object):
             self.local_table,
             **self.mapper_args
         )
-        del mp_.class_manager.info['declared_attr_reg']
+        del self.cls._sa_declared_attr_reg
         return mp_
 
 
index 924130874f289f6afd70d6c8545494edd8fa7275..468846d40722d803cc8f2ea5a10a70b3c9d75c11 100644 (file)
@@ -1096,8 +1096,6 @@ class Mapper(InspectionAttr):
 
         """
 
-        # when using declarative as of 1.0, the register_class has
-        # already happened from within declarative.
         manager = attributes.manager_of_class(self.class_)
 
         if self.non_primary:
@@ -1120,14 +1118,20 @@ class Mapper(InspectionAttr):
                     "create a non primary Mapper.  clear_mappers() will "
                     "remove *all* current mappers from all classes." %
                     self.class_)
-
-        if manager is None:
-            manager = instrumentation.register_class(self.class_)
+            # else:
+                # a ClassManager may already exist as
+                # ClassManager.instrument_attribute() creates
+                # new managers for each subclass if they don't yet exist.
 
         _mapper_registry[self] = True
 
+        # note: this *must be called before instrumentation.register_class*
+        # to maintain the documented behavior of instrument_class
         self.dispatch.instrument_class(self, self.class_)
 
+        if manager is None:
+            manager = instrumentation.register_class(self.class_)
+
         self.class_manager = manager
 
         manager.mapper = self
index 3fac39cacf6a4aa8a8320146591ef453fd8f807a..ab0de801ce2340e54386738ac07973624b0d7bbd 100644 (file)
@@ -13,7 +13,10 @@ from sqlalchemy.orm import relationship, create_session, class_mapper, \
     column_property, composite, Session, properties
 from sqlalchemy.util import with_metaclass
 from sqlalchemy.ext.declarative import declared_attr, synonym_for
-from sqlalchemy.testing import fixtures
+from sqlalchemy.testing import fixtures, mock
+from sqlalchemy.orm.events import MapperEvents
+from sqlalchemy.orm import mapper
+from sqlalchemy import event
 
 Base = None
 
@@ -1671,6 +1674,32 @@ class DeclarativeTest(DeclarativeTestBase):
             ))
         )
 
+    @testing.teardown_events(MapperEvents)
+    def test_instrument_class_before_instrumentation(self):
+        # test #3388
+
+        canary = mock.Mock()
+
+        @event.listens_for(mapper, "instrument_class")
+        def instrument_class(mp, cls):
+            canary.instrument_class(mp, cls)
+
+        @event.listens_for(object, "class_instrument")
+        def class_instrument(cls):
+            canary.class_instrument(cls)
+
+        class Test(Base):
+            __tablename__ = 'test'
+            id = Column(Integer, primary_key=True)
+        # MARKMARK
+        eq_(
+            canary.mock_calls,
+            [
+                mock.call.instrument_class(Test.__mapper__, Test),
+                mock.call.class_instrument(Test)
+            ]
+        )
+
 
 def _produce_test(inline, stringbased):
 
index 89cc54eb82c909bbafa54d436cb416b618a1bc6d..51a0bb3a2dfb7416f1bfa754a4af43cb5c022d87 100644 (file)
@@ -189,13 +189,13 @@ class MapperEventsTest(_RemoveListeners, _fixtures.FixtureTest):
             ]
         )
 
-
     def test_merge(self):
         users, User = self.tables.users, self.classes.User
 
         mapper(User, users)
 
         canary = []
+
         def load(obj, ctx):
             canary.append('load')
         event.listen(mapper, 'load', load)
@@ -212,6 +212,7 @@ class MapperEventsTest(_RemoveListeners, _fixtures.FixtureTest):
         s.query(User).order_by(User.id).first()
         eq_(canary, ['load', 'load', 'load'])
 
+
     def test_inheritance(self):
         users, addresses, User = (self.tables.users,
                                 self.tables.addresses,
@@ -385,6 +386,43 @@ class MapperEventsTest(_RemoveListeners, _fixtures.FixtureTest):
         mapper(Address, addresses)
         eq_(canary, [User, Address])
 
+    def test_instrument_class_precedes_class_instrumentation(self):
+        users = self.tables.users
+
+        class MyClass(object):
+            pass
+
+        canary = Mock()
+
+        def my_init(self):
+            canary.init()
+
+        # mapper level event
+        @event.listens_for(mapper, "instrument_class")
+        def instrument_class(mp, class_):
+            canary.instrument_class(class_)
+            class_.__init__ = my_init
+
+        # instrumentationmanager event
+        @event.listens_for(object, "class_instrument")
+        def class_instrument(class_):
+            canary.class_instrument(class_)
+
+        mapper(MyClass, users)
+
+        m1 = MyClass()
+        assert attributes.instance_state(m1)
+
+        eq_(
+            [
+                call.instrument_class(MyClass),
+                call.class_instrument(MyClass),
+                call.init()
+            ],
+            canary.mock_calls
+        )
+
+
 class DeclarativeEventListenTest(_RemoveListeners, fixtures.DeclarativeMappedTest):
     run_setup_classes = "each"
     run_deletes = None