From d46985d699e6ebffe45c94d91cfa842271e06bb0 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 2 Oct 2010 13:31:07 -0400 Subject: [PATCH] - add instrumentation events - simplify listen_for_events example with new system - add "propagate", "retval", "raw" flags to attribute events. this solves the "return value" issue as well as the "subclass" issue. - begin thinking about event removal. Each listen() method will have a corresponding remove(). Custom listen() methods will have to package all the info onto the event function that is needed to remove its state. --- .../custom_attributes/listen_for_events.py | 50 ++--- lib/sqlalchemy/event.py | 33 ++- lib/sqlalchemy/orm/attributes.py | 76 +------ lib/sqlalchemy/orm/deprecated_interfaces.py | 10 +- lib/sqlalchemy/orm/events.py | 208 ++++++++++++++++-- lib/sqlalchemy/orm/instrumentation.py | 12 +- lib/sqlalchemy/orm/mapper.py | 22 +- lib/sqlalchemy/orm/session.py | 2 +- lib/sqlalchemy/orm/state.py | 13 +- lib/sqlalchemy/orm/util.py | 2 +- test/orm/test_instrumentation.py | 10 +- 11 files changed, 277 insertions(+), 161 deletions(-) diff --git a/examples/custom_attributes/listen_for_events.py b/examples/custom_attributes/listen_for_events.py index 6d467fbbc1..e66ebd0905 100644 --- a/examples/custom_attributes/listen_for_events.py +++ b/examples/custom_attributes/listen_for_events.py @@ -1,42 +1,25 @@ """ -Illustrates how to use AttributeExtension to listen for change events -across the board. +Illustrates how to attach events to all instrumented attributes +and listen for change events. """ -from sqlalchemy.orm.interfaces import AttributeExtension, \ - InstrumentationManager +from sqlalchemy import event, orm -class InstallListeners(InstrumentationManager): - def post_configure_attribute(self, class_, key, inst): - """Add an event listener to an InstrumentedAttribute.""" - - inst.impl.extensions.insert(0, AttributeListener(key)) - -class AttributeListener(AttributeExtension): - """Generic event listener. - - Propagates attribute change events to a - "receive_change_event()" method on the target - instance. - - """ - def __init__(self, key): - self.key = key - - def append(self, state, value, initiator): - self._report(state, value, None, "appended") - return value +def configure_listener(class_, key, inst): + def append(instance, value, initiator): + instance.receive_change_event("append", key, value, None) - def remove(self, state, value, initiator): - self._report(state, value, None, "removed") + def remove(instance, value, initiator): + instance.receive_change_event("remove", key, value, None) + + def set_(instance, value, oldvalue, initiator): + instance.receive_change_event("set", key, value, oldvalue) + + event.listen(append, 'on_append', inst) + event.listen(remove, 'on_remove', inst) + event.listen(set_, 'on_set', inst) - def set(self, state, value, oldvalue, initiator): - self._report(state, value, oldvalue, "set") - return value - - def _report(self, state, value, oldvalue, verb): - state.obj().receive_change_event(verb, self.key, value, oldvalue) if __name__ == '__main__': @@ -45,7 +28,6 @@ if __name__ == '__main__': from sqlalchemy.ext.declarative import declarative_base class Base(object): - __sa_instrumentation_manager__ = InstallListeners def receive_change_event(self, verb, key, value, oldvalue): s = "Value '%s' %s on attribute '%s', " % (value, verb, key) @@ -56,6 +38,8 @@ if __name__ == '__main__': Base = declarative_base(cls=Base) + event.listen(configure_listener, 'on_attribute_instrument', Base) + class MyMappedClass(Base): __tablename__ = "mytable" diff --git a/lib/sqlalchemy/event.py b/lib/sqlalchemy/event.py index c1bc54b177..c7df2bf485 100644 --- a/lib/sqlalchemy/event.py +++ b/lib/sqlalchemy/event.py @@ -29,7 +29,20 @@ def listen(fn, identifier, target, *args, **kw): return raise exc.InvalidRequestError("No such event %s for target %s" % (identifier,target)) + +def remove(fn, identifier, target): + """Remove an event listener. + + Note that some event removals, particularly for those event dispatchers + which create wrapper functions and secondary even listeners, may not yet + be supported. + """ + for evt_cls in _registrars[identifier]: + for tgt in evt_cls.accept_with(target): + tgt.dispatch.remove(fn, identifier, tgt, *args, **kw) + + _registrars = util.defaultdict(list) class _Dispatch(object): @@ -105,13 +118,11 @@ class Events(object): @classmethod def listen(cls, fn, identifier, target): getattr(target.dispatch, identifier).append(fn, target) - -# def update(self, other): -# """Populate from the listeners in another :class:`Events` object.""" - -# for ls in other.events: -# getattr(self, ls.name).listeners.extend(ls.listeners) + @classmethod + def remove(cls, fn, identifier, target): + getattr(target.dispatch, identifier).remove(fn, target) + class _DispatchDescriptor(object): """Class-level attributes on _Dispatch classes.""" @@ -127,6 +138,10 @@ class _DispatchDescriptor(object): for cls in [target] + target.__subclasses__(): self._clslevel[cls].append(obj) + def remove(self, obj, target): + for cls in [target] + target.__subclasses__(): + self._clslevel[cls].remove(obj) + def __get__(self, obj, cls): if obj is None: return self @@ -180,7 +195,11 @@ class _ListenerCollection(object): def append(self, obj, target): if obj not in self.listeners: self.listeners.append(obj) - + + def remove(self, obj, target): + if obj in self.listeners: + self.listeners.remove(obj) + class dispatcher(object): """Descriptor used by target classes to deliver the _Dispatch class at the class level diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index 46b5a5dca7..6bc15d8f5e 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -16,7 +16,7 @@ import operator from operator import itemgetter from sqlalchemy import util, event -from sqlalchemy.orm import interfaces, collections +from sqlalchemy.orm import interfaces, collections, events import sqlalchemy.exceptions as sa_exc # lazy imports @@ -52,75 +52,17 @@ PASSIVE_OFF = False #util.symbol('PASSIVE_OFF') """Symbol indicating that loader callables should be executed.""" -class AttributeEvents(event.Events): - """Events for ORM attributes. - - e.g.:: - - from sqlalchemy import event - event.listen(listener, 'on_append', MyClass.collection) - event.listen(listener, 'on_set', MyClass.some_scalar, - active_history=True) - - active_history = True indicates that the "on_set" event would like - to receive the 'old' value, even if it means firing lazy callables. - - """ - - # TODO: what to do about subclasses !! - # a shared approach will be needed. listeners can be placed - # before subclasses are created. new attrs on subclasses - # can pull them from the superclass attr. listeners - # should be auto-propagated to existing subclasses. - - @classmethod - def listen(cls, fn, identifier, target, active_history=False): - if active_history: - target.dispatch.active_history = True - event.Events.listen(fn, identifier, target) - - @classmethod - def unwrap(cls, identifier, event): - return event['value'] - - def on_append(self, state, value, initiator): - """Receive a collection append event. - - The returned value will be used as the actual value to be - appended. - - """ - - def on_remove(self, state, value, initiator): - """Receive a remove event. - - No return value is defined. - - """ - - def on_set(self, state, value, oldvalue, initiator): - """Receive a set event. - - The returned value will be used as the actual value to be - set. - - """ - class QueryableAttribute(interfaces.PropComparator): - - def __init__(self, key, impl=None, comparator=None, parententity=None): - """Construct an InstrumentedAttribute. - - comparator - a sql.Comparator to which class-level compare/math events will be - sent - """ + """Base class for class-bound attributes. """ + + def __init__(self, class_, key, impl=None, comparator=None, parententity=None): + self.class_ = class_ self.key = key self.impl = impl self.comparator = comparator self.parententity = parententity - dispatch = event.dispatcher(AttributeEvents) + dispatch = event.dispatcher(events.AttributeEvents) dispatch.dispatch_cls.active_history = False def get_history(self, instance, **kwargs): @@ -166,7 +108,7 @@ class QueryableAttribute(interfaces.PropComparator): class InstrumentedAttribute(QueryableAttribute): - """Public-facing descriptor, placed in the mapped class dictionary.""" + """Class bound instrumented attribute which adds descriptor methods.""" def __set__(self, instance, value): self.impl.set(instance_state(instance), @@ -1121,7 +1063,7 @@ def register_attribute_impl(class_, key, impl = ScalarAttributeImpl(class_, key, callable_, dispatch, **kw) manager[key].impl = impl - + manager.post_configure_attribute(key) @@ -1133,7 +1075,7 @@ def register_descriptor(class_, key, proxy_property=None, comparator=None, proxy_type = proxied_attribute_factory(proxy_property) descriptor = proxy_type(key, proxy_property, comparator, parententity) else: - descriptor = InstrumentedAttribute(key, comparator=comparator, + descriptor = InstrumentedAttribute(class_, key, comparator=comparator, parententity=parententity) descriptor.__doc__ = doc diff --git a/lib/sqlalchemy/orm/deprecated_interfaces.py b/lib/sqlalchemy/orm/deprecated_interfaces.py index fc5a74a32e..86f6ed74ec 100644 --- a/lib/sqlalchemy/orm/deprecated_interfaces.py +++ b/lib/sqlalchemy/orm/deprecated_interfaces.py @@ -385,12 +385,14 @@ class AttributeExtension(object): @classmethod def _adapt_listener(cls, self, listener): event.listen(listener.append, 'on_append', self, - active_history=listener.active_history) + active_history=listener.active_history, + raw=True, retval=True) event.listen(listener.remove, 'on_remove', self, - active_history=listener.active_history) + active_history=listener.active_history, + raw=True, retval=True) event.listen(listener.set, 'on_set', self, - active_history=listener.active_history) - + active_history=listener.active_history, + raw=True, retval=True) def append(self, state, value, initiator): """Receive a collection append event. diff --git a/lib/sqlalchemy/orm/events.py b/lib/sqlalchemy/orm/events.py index d9246c83eb..29274ac3b5 100644 --- a/lib/sqlalchemy/orm/events.py +++ b/lib/sqlalchemy/orm/events.py @@ -1,12 +1,67 @@ """ORM event interfaces. """ -from sqlalchemy import event +from sqlalchemy import event, util, exc -class ClassEvents(event.Events): +class InstrumentationEvents(event.Events): + """Events related to class instrumentation events. + + The listeners here support being established against + any new style class, that is any object that is a subclass + of 'type'. Events will then be fired off for events + against that class as well as all subclasses. + 'type' itself is also accepted as a target + in which case the events fire for all classes. + + """ + @classmethod def accept_with(cls, target): - from sqlalchemy.orm.instrumentation import ClassManager + from sqlalchemy.orm.instrumentation import instrumentation_registry + + if isinstance(target, type): + return [instrumentation_registry] + else: + return [] + + @classmethod + def listen(cls, fn, identifier, target): + + @util.decorator + def adapt_to_target(fn, cls, *arg): + if issubclass(cls, target): + fn(cls, *arg) + event.Events.listen(fn, identifier, target) + + @classmethod + def remove(cls, fn, identifier, target): + raise NotImplementedError("Removal of instrumentation events not yet implemented") + + def on_class_instrument(self, cls): + """Called after the given class is instrumented. + + To get at the :class:`.ClassManager`, use + :func:`.manager_of_class`. + + """ + + def on_class_uninstrument(self, cls): + """Called before the given class is uninstrumented. + + To get at the :class:`.ClassManager`, use + :func:`.manager_of_class`. + + """ + + + def on_attribute_instrument(self, cls, key, inst): + """Called when an attribute is instrumented.""" + +class InstanceEvents(event.Events): + + @classmethod + def accept_with(cls, target): + from sqlalchemy.orm.instrumentation import ClassManager, manager_of_class if isinstance(target, ClassManager): return [target] @@ -15,36 +70,147 @@ class ClassEvents(event.Events): if manager: return [manager] return [] + + @classmethod + def listen(cls, fn, identifier, target, raw=False): + if not raw: + fn = _to_instance(fn) + event.Events.listen(fn, identifier, target) + + @classmethod + def remove(cls, fn, identifier, target): + raise NotImplementedError("Removal of instance events not yet implemented") - # TODO: change these to accept "target" - - # the target is the state or the instance, depending - # on if the listener was registered with "raw=True" - - # do the same thing for all the other events here (Mapper, Session, Attributes). - # Not sending raw=True means the listen() method of the - # Events subclass will wrap incoming listeners to marshall each - # "target" argument into "instance". The name "target" can be - # used consistently to make it simple. - # - # this way end users don't have to deal with InstanceState and - # the internals can have faster performance. - - def on_init(self, state, instance, args, kwargs): + def on_init(self, target, args, kwargs): """""" - def on_init_failure(self, state, instance, args, kwargs): + def on_init_failure(self, target, args, kwargs): """""" - def on_load(self, instance): + def on_load(self, target): """""" - def on_resurrect(self, state, instance): + def on_resurrect(self, target): """""" + class MapperEvents(event.Events): """""" + @classmethod + def remove(cls, fn, identifier, target): + raise NotImplementedError("Removal of mapper events not yet implemented") class SessionEvents(event.Events): """""" - + @classmethod + def remove(cls, fn, identifier, target): + raise NotImplementedError("Removal of session events not yet implemented") + class AttributeEvents(event.Events): - """""" \ No newline at end of file + """Define events for object attributes. + + e.g.:: + + from sqlalchemy import event + event.listen(my_append_listener, 'on_append', MyClass.collection) + event.listen(my_set_listener, 'on_set', + MyClass.somescalar, retval=True) + + Several modifiers are available to the listen() function. + + :param active_history=False: When True, indicates that the + "on_set" event would like to receive the "old" value + being replaced unconditionally, even if this requires + firing off database loads. + :param propagate=False: When True, the listener function will + be established not just for the class attribute given, but + for attributes of the same name on all current subclasses + of that class, as well as all future subclasses of that + class, using an additional listener that listens for + instrumentation events. + :param raw=False: When True, the "target" argument to the + event will be the :class:`.InstanceState` management + object, rather than the mapped instance itself. + :param retval=False:` when True, the user-defined event + listening must return the "value" argument from the + function. This gives the listening function the opportunity + to change the value that is ultimately used for a "set" + or "append" event. + + """ + + @classmethod + def listen(cls, fn, identifier, target, active_history=False, + raw=False, retval=False, + propagate=False): + if active_history: + target.dispatch.active_history = True + + # TODO: for removal, need to package the identity + # of the wrapper with the original function. + + if raw is False or retval is False: + @util.decorator + def wrap(fn, target, value, *arg): + if not raw: + target = target.obj() + if not retval: + fn(target, value, *arg) + return value + else: + return fn(target, value, *arg) + fn = wrap(fn) + + event.Events.listen(fn, identifier, target) + + if propagate: + # TODO: for removal, need to implement + # packaging this info for operation in reverse. + + class_ = target.class_ + for cls in class_.__subclasses__(): + impl = getattr(cls, target.key) + if impl is not target: + event.Events.listen(fn, identifier, impl) + + def configure_listener(class_, key, inst): + event.Events.listen(fn, identifier, inst) + event.listen(configure_listener, 'on_attribute_instrument', class_) + + @classmethod + def remove(cls, fn, identifier, target): + raise NotImplementedError("Removal of attribute events not yet implemented") + + @classmethod + def unwrap(cls, identifier, event): + return event['value'] + + def on_append(self, state, value, initiator): + """Receive a collection append event. + + The returned value will be used as the actual value to be + appended. + + """ + + def on_remove(self, state, value, initiator): + """Receive a remove event. + + No return value is defined. + + """ + + def on_set(self, state, value, oldvalue, initiator): + """Receive a set event. + + The returned value will be used as the actual value to be + set. + + """ + +@util.decorator +def _to_instance(fn, state, *arg, **kw): + """Marshall the :class:`.InstanceState` argument to an instance.""" + + return fn(state.obj(), *arg, **kw) + diff --git a/lib/sqlalchemy/orm/instrumentation.py b/lib/sqlalchemy/orm/instrumentation.py index 3f134c58ab..02ba5e1a22 100644 --- a/lib/sqlalchemy/orm/instrumentation.py +++ b/lib/sqlalchemy/orm/instrumentation.py @@ -92,7 +92,7 @@ class ClassManager(dict): self.manage() self._instrument_init() - dispatch = event.dispatcher(events.ClassEvents) + dispatch = event.dispatcher(events.InstanceEvents) @property def is_mapped(self): @@ -195,7 +195,8 @@ class ClassManager(dict): manager.instrument_attribute(key, inst, True) def post_configure_attribute(self, key): - pass + instrumentation_registry.dispatch.\ + on_attribute_instrument(self.class_, key, self[key]) def uninstrument_attribute(self, key, propagated=False): if key not in self: @@ -360,6 +361,7 @@ class _ClassInstrumentationAdapter(ClassManager): self._adapted.instrument_attribute(self.class_, key, inst) def post_configure_attribute(self, key): + super(_ClassInstrumentationAdpter, self).post_configure_attribute(key) self._adapted.post_configure_attribute(self.class_, key, self[key]) def install_descriptor(self, key, inst): @@ -470,6 +472,8 @@ class InstrumentationRegistry(object): _dict_finders = util.WeakIdentityMapping() _extended = False + dispatch = event.dispatcher(events.InstrumentationEvents) + def create_manager_for_cls(self, class_, **kw): assert class_ is not None assert manager_of_class(class_) is None @@ -506,6 +510,9 @@ class InstrumentationRegistry(object): self._manager_finders[class_] = manager.manager_getter() self._state_finders[class_] = manager.state_getter() self._dict_finders[class_] = manager.dict_getter() + + self.dispatch.on_class_instrument(class_) + return manager def _collect_management_factories_for(self, cls): @@ -572,6 +579,7 @@ class InstrumentationRegistry(object): def unregister(self, class_): if class_ in self._manager_finders: manager = self.manager_of_class(class_) + self.dispatch.on_class_uninstrument(class_) manager.unregister() manager.dispose() del self._manager_finders[class_] diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index b3aa7fb294..a400216633 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -405,14 +405,14 @@ class Mapper(object): if manager.info.get(_INSTRUMENTOR, False): return - event.listen(_event_on_init, 'on_init', manager) - event.listen(_event_on_init_failure, 'on_init_failure', manager) - event.listen(_event_on_resurrect, 'on_resurrect', manager) + event.listen(_event_on_init, 'on_init', manager, raw=True) + event.listen(_event_on_init_failure, 'on_init_failure', manager, raw=True) + event.listen(_event_on_resurrect, 'on_resurrect', manager, raw=True) for key, method in util.iterate_attributes(self.class_): if isinstance(method, types.FunctionType): if hasattr(method, '__sa_reconstructor__'): - event.listen(method, 'on_load', manager) + event.listen(method, 'on_load', manager, raw=True) elif hasattr(method, '__sa_validators__'): for name in method.__sa_validators__: self._validators[name] = method @@ -420,7 +420,7 @@ class Mapper(object): if 'reconstruct_instance' in self.extension: def reconstruct(instance): self.extension.reconstruct_instance(self, instance) - event.listen(reconstruct, 'on_load', manager) + event.listen(reconstruct, 'on_load', manager, raw=False) manager.info[_INSTRUMENTOR] = self @@ -2296,7 +2296,7 @@ class Mapper(object): populate_state(state, dict_, row, isnew, attrs) if loaded_instance: - state._run_on_load(instance) + state._run_on_load() if result is not None and \ (not append_result or @@ -2394,7 +2394,7 @@ def validates(*names): return fn return wrap -def _event_on_init(state, instance, args, kwargs): +def _event_on_init(state, args, kwargs): """Trigger mapper compilation and run init_instance hooks.""" instrumenting_mapper = state.manager.info[_INSTRUMENTOR] @@ -2404,9 +2404,9 @@ def _event_on_init(state, instance, args, kwargs): instrumenting_mapper.extension.init_instance( instrumenting_mapper, instrumenting_mapper.class_, state.manager.original_init, - instance, args, kwargs) + state.obj(), args, kwargs) -def _event_on_init_failure(state, instance, args, kwargs): +def _event_on_init_failure(state, args, kwargs): """Run init_failed hooks.""" instrumenting_mapper = state.manager.info[_INSTRUMENTOR] @@ -2414,9 +2414,9 @@ def _event_on_init_failure(state, instance, args, kwargs): util.warn_exception( instrumenting_mapper.extension.init_failed, instrumenting_mapper, instrumenting_mapper.class_, - state.manager.original_init, instance, args, kwargs) + state.manager.original_init, state.obj(), args, kwargs) -def _event_on_resurrect(state, instance): +def _event_on_resurrect(state): # re-populate the primary key elements # of the dict based on the mapping. instrumenting_mapper = state.manager.info[_INSTRUMENTOR] diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 95d29812e5..3444c12ac0 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -1243,7 +1243,7 @@ class Session(object): merged_state.commit_all(merged_dict, self.identity_map) if new_instance: - merged_state._run_on_load(merged) + merged_state._run_on_load() return merged @classmethod diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py index a8f03102da..42fc5b98ee 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -96,7 +96,7 @@ class InstanceState(object): self, instance, args = mixed[0], mixed[1], mixed[2:] manager = self.manager - manager.dispatch.on_init(self, instance, args, kwargs) + manager.dispatch.on_init(self, args, kwargs) # LESSTHANIDEAL: # adjust for the case where the InstanceState was created before @@ -109,7 +109,7 @@ class InstanceState(object): try: return manager.original_init(*mixed[1:], **kwargs) except: - manager.dispatch.on_init_failure(self, instance, args, kwargs) + manager.dispatch.on_init_failure(self, args, kwargs) raise def get_history(self, key, **kwargs): @@ -142,8 +142,8 @@ class InstanceState(object): else: return [x] - def _run_on_load(self, instance): - self.manager.dispatch.on_load(instance) + def _run_on_load(self): + self.manager.dispatch.on_load(self) def __getstate__(self): d = {'instance':self.obj()} @@ -511,11 +511,6 @@ class MutableAttrInstanceState(InstanceState): # re-establishes identity attributes from the key self.manager.dispatch.on_resurrect(self, obj) - # TODO: don't really think we should run this here. - # resurrect is only meant to preserve the minimal state needed to - # do an UPDATE, not to produce a fully usable object - self._run_on_load(obj) - return obj class PendingCollection(object): diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index f79a8449fc..8ec161e995 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -341,7 +341,7 @@ class AliasedClass(object): existing = getattr(self.__target, prop.key) comparator = existing.comparator.adapted(self.__adapt_element) - queryattr = attributes.QueryableAttribute(prop.key, + queryattr = attributes.QueryableAttribute(self, prop.key, impl=existing.impl, parententity=self, comparator=comparator) setattr(self, prop.key, queryattr) return queryattr diff --git a/test/orm/test_instrumentation.py b/test/orm/test_instrumentation.py index 0653c15e90..7ffee2a2e5 100644 --- a/test/orm/test_instrumentation.py +++ b/test/orm/test_instrumentation.py @@ -5,7 +5,7 @@ from sqlalchemy import MetaData, Integer, ForeignKey, util, event from sqlalchemy.test.schema import Table from sqlalchemy.test.schema import Column from sqlalchemy.orm import mapper, relationship, create_session, \ - attributes, class_mapper, clear_mappers, instrumentation + attributes, class_mapper, clear_mappers, instrumentation, events from sqlalchemy.test.testing import eq_, ne_ from sqlalchemy.util import function_named from test.orm import _base @@ -46,9 +46,9 @@ class InitTest(_base.ORMTest): instrumentation.register_class(cls) ne_(cls.__init__, original_init) manager = instrumentation.manager_of_class(cls) - def on_init(state, instance, args, kwargs): - canary.append((cls, 'on_init', type(instance))) - event.listen(on_init, 'on_init', manager) + def on_init(state, args, kwargs): + canary.append((cls, 'on_init', state.class_)) + event.listen(on_init, 'on_init', manager, raw=True) def test_ai(self): inits = [] @@ -573,7 +573,7 @@ class ExtendedEventsTest(_base.ORMTest): @modifies_instrumentation_finders def test_subclassed(self): - class MyEvents(instrumentation.ClassEvents): + class MyEvents(events.InstanceEvents): pass class MyClassManager(instrumentation.ClassManager): dispatch = event.dispatcher(MyEvents) -- 2.47.2