.. 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
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:
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()
self.local_table,
**self.mapper_args
)
- del mp_.class_manager.info['declared_attr_reg']
+ del self.cls._sa_declared_attr_reg
return mp_
"""
- # 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:
"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
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
))
)
+ @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):
]
)
-
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)
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,
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