Commits

Olemis Lang committed 420ff8f

Trac #11148 : Generic event listeners supporting custom event data

  • Participants
  • Parent commits 7914ff3
  • Branches trac_t11148

Comments (0)

Files changed (1)

File t11148/t11148_r11782_IEntityChangeListener_extends_with_prefix.diff

 
 diff -r 8d0c3223a818 trac/core.py
 --- a/trac/core.py	Thu Apr 18 14:30:21 2013 +0000
-+++ b/trac/core.py	Tue Apr 23 10:13:04 2013 -0500
++++ b/trac/core.py	Tue Apr 23 13:09:41 2013 -0500
 @@ -165,8 +165,30 @@
  
          locals_.setdefault('_implements', []).extend(interfaces)
  
  
  class ComponentManager(object):
-@@ -236,3 +258,63 @@
+@@ -236,3 +258,68 @@
          with the given class will not be available.
          """
          return True
 +                       None.
 +    :param     kwargs: receives in the form of excess keyword arguments 
 +                       custom event data supported by resource-specific
-+                       interfaces 
++                       interfaces. 
 +    """
 +
 +    def entity_created(entity, changeinfo = None, **kwargs):
 +        xp = ExtensionPoint(interface)
 +        # FIXME : Sender component rather than self. Change method signature
 +        for listener in xp.extensions(self):
-+            getattr(listener, method_name)(**kwargs)
++            # FIXME : Do not copy kwargs
++            getattr(listener, method_name)(**kwargs.copy())
 +
 +        xp = ExtensionPoint(IEntityChangeListener)
-+        parts = method_name.split('_', 1)
-+        method_name = 'entity_' + parts[-1]
-+        for listener in xp.extensions(self):
-+            getattr(listener, method_name)(**kwargs)
++        generic_listeners = xp.extensions(self)
++        if generic_listeners:
++            #TODO : Include `interface` and `method_name` in kwargs ?
++            parts = method_name.split('_', 1)
++            method_name = 'entity_' + parts[-1]
++            for listener in generic_listeners:
++                # FIXME : Do not copy kwargs
++                getattr(listener, method_name)(**kwargs.copy())
 diff -r 8d0c3223a818 trac/ticket/model.py
 --- a/trac/ticket/model.py	Thu Apr 18 14:30:21 2013 +0000
-+++ b/trac/ticket/model.py	Tue Apr 23 10:13:04 2013 -0500
++++ b/trac/ticket/model.py	Tue Apr 23 13:09:41 2013 -0500
 @@ -25,7 +25,8 @@
  from trac.attachment import Attachment
  from trac import core
          """
 diff -r 8d0c3223a818 trac/ticket/tests/model.py
 --- a/trac/ticket/tests/model.py	Thu Apr 18 14:30:21 2013 +0000
-+++ b/trac/ticket/tests/model.py	Tue Apr 23 10:13:04 2013 -0500
-@@ -12,8 +12,8 @@
- from trac.core import TracError, implements
++++ b/trac/ticket/tests/model.py	Tue Apr 23 13:09:41 2013 -0500
+@@ -9,11 +9,11 @@
+ 
+ from trac import core
+ from trac.attachment import Attachment
+-from trac.core import TracError, implements
++from trac.core import implements, TracError
  from trac.resource import ResourceNotFound
  from trac.ticket.model import (
 -    Ticket, Component, Milestone, Priority, Type, Version
  from trac.ticket.api import (
      IMilestoneChangeListener, ITicketChangeListener, TicketSystem
  )
-@@ -1097,6 +1097,232 @@
+@@ -1097,6 +1097,390 @@
          self.assertEqual([('Test', 0, 'Some text')], self.env.db_query(
              "SELECT name, time, description FROM version WHERE name='Test'"))
  
 +        self.assertTrue(hasattr(IPriorityChangeListener, 'priority_deleted'))
 +        self.assertTrue(hasattr(IPriorityChangeListener, 'priority_reparented'))
 +
++
 +class MultipleEntitiesChangeListenerMock(core.Component):
 +    implements(IComponentChangeListener, IVersionChangeListener)
 +
 +    def version_reparented(self, entity, changeinfo = None):
 +        pass
 +
++
 +class MultipleEntitiesChangeListenerTestCase(unittest.TestCase):
 +
 +    def setUp(self):
 +    def test_can_receive_events_from_different_entities(self):
 +        self._create_entity(Component, "Component1")
 +        self._create_entity(Version, "Version1")
-+        self.assertEqual(
-+            ["component_created", "version_created"], self.listener.action)
++        self.assertEqual(["component_created", "version_created"], 
++                         self.listener.action)
 +
 +    def _create_entity(self, entity_type, name):
 +        entity = entity_type(self.env)
 +        entity.insert()
 +        return entity
 +
++
++class GenericEntitiesChangeListenerMock(core.Component):
++    implements(core.IEntityChangeListener)
++
++    def __init__(self):
++        self.details = []
++        self.action = []
++
++    def _handle(self, kwargs):
++        cls = kwargs['entity'].__class__
++        self.action.append(cls.__name__.lower() + '_' + kwargs['action'])
++        self.details.append(kwargs)
++
++    def entity_created(self, entity, changeinfo = None, **kwargs):
++        kwargs.update(entity=entity, changeinfo=changeinfo, action='created')
++        self._handle(kwargs)
++
++    def entity_changed(self, entity, old_values, changeinfo = None, **kwargs):
++        kwargs.update(entity=entity, changeinfo=changeinfo, action='changed',
++                      old_values=old_values)
++        self._handle(kwargs)
++
++    def entity_deleted(self, entity, changeinfo = None, **kwargs):
++        kwargs.update(entity=entity, changeinfo=changeinfo, action='deleted')
++        self._handle(kwargs)
++
++    def entity_reparented(self, entity, changeinfo = None, **kwargs):
++        kwargs.update(entity=entity, changeinfo=changeinfo, action='reparented')
++        self._handle(kwargs)
++
++
++class GenericEntitiesChangeListenerTestCase(MultipleEntitiesChangeListenerTestCase):
++
++    def setUp(self):
++        self.env = EnvironmentStub(default_data=True)
++        self.listener = GenericEntitiesChangeListenerMock(self.env)
++
++    def test_custom_event_args(self):
++
++        class ISomethingListener(core.Interface):
++            """Listener interface written from scratch
++            """
++            def something_created(entity, changeinfo = None):
++                """Called when an entity is created."""
++
++            def something_changed(entity, old_values, required_arg,
++                                  changeinfo=None, optional_arg=None):
++                """Update event with required and optional custom args."""
++
++            def something_deleted(entity, changeinfo = None):
++                """Called when an entity is deleted."""
++
++            def something_reparented(entity, changeinfo = None):
++                """Never invoked. May be removed."""
++
++        class Something(object):
++            def __init__(self, env):
++                self.env = env
++                self.required_arg = 999
++
++            # Just trigger notifications
++            def delete(self, db=None):
++                notifier = core.ListenerNotifier(self.env)
++                notifier.notify(ISomethingListener.something_deleted, 
++                                entity=self)
++
++            def insert(self, db=None):
++                notifier = core.ListenerNotifier(self.env)
++                notifier.notify(ISomethingListener.something_created,
++                                entity=self)
++
++            def update(self, db=None):
++                notifier = core.ListenerNotifier(self.env)
++                optional_args = dict(optional_arg=self.optional_arg) \
++                                if hasattr(self, 'optional_arg') \
++                                else {}
++                notifier.notify(ISomethingListener.something_changed,
++                                entity=self,
++                                old_values=dict(),
++                                required_arg=self.required_arg,
++                                **optional_args)
++
++        class SomethingListener(core.Component):
++            """Replicate the same behavior of generic listener
++            """
++            implements(ISomethingListener)
++
++            def __init__(self):
++                self.details = []
++
++            def _handle(self, kwargs):
++                if 'optional_arg' in kwargs and kwargs['optional_arg'] is None:
++                    del kwargs['optional_arg']
++                self.details.append(kwargs)
++
++            def something_created(self, entity, changeinfo=None):
++                args = locals()
++                del args['self']
++                args['action'] = 'created'
++                self._handle(args)
++
++            def something_changed(self, entity, old_values, required_arg,
++                                  changeinfo=None, optional_arg=None):
++                args = locals()
++                del args['self']
++                args['action'] = 'changed'
++                self._handle(args)
++
++            def something_deleted(self, entity, changeinfo=None):
++                args = locals()
++                del args['self']
++                args['action'] = 'deleted'
++                self._handle(args)
++
++            def something_reparented(self, entity, changeinfo=None):
++                args = locals()
++                del args['self']
++                args['action'] = 'reparented'
++                self._handle(args)
++
++        resource_listener = SomethingListener(self.env)
++
++        # CRUD operations
++        something = Something(self.env)
++        something.insert()
++        something.update()
++        something.required_arg = 7
++        something.optional_arg = 44
++        something.update()
++        something.delete()
++
++        expected = [
++                    {'action' : 'created',
++                     'entity' : something,
++                     'changeinfo' : None},
++                    {'action' : 'changed',
++                     'entity' : something,
++                     'old_values' : {},
++                     'required_arg' : 999,
++                     'changeinfo' : None},
++                    {'action' : 'changed',
++                     'entity' : something,
++                     'old_values' : {},
++                     'required_arg' : 7,
++                     'optional_arg' : 44,
++                     'changeinfo' : None},
++                    {'action' : 'deleted',
++                     'entity' : something,
++                     'changeinfo' : None},
++                    ]
++
++        self.assertEquals(expected, self.listener.details,
++                          'Generic listener event sequence')
++        self.assertEquals(expected, resource_listener.details,
++                          'Resource-specific listener event sequence')
++
  
  def suite():
      suite = unittest.TestSuite()
-@@ -1107,6 +1333,14 @@
+@@ -1107,6 +1491,17 @@
      suite.addTest(unittest.makeSuite(MilestoneTestCase, 'test'))
      suite.addTest(unittest.makeSuite(ComponentTestCase, 'test'))
      suite.addTest(unittest.makeSuite(VersionTestCase, 'test'))
 +        PriorityEntityChangeListenerTestCase, 'test'))
 +    suite.addTest(unittest.makeSuite(
 +        MultipleEntitiesChangeListenerTestCase, 'test'))
++    suite.addTest(unittest.makeSuite(
++        GenericEntitiesChangeListenerTestCase, 'test'))
++
      return suite
  
  if __name__ == '__main__':