Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inspect and remove event handlers on Models #36

Merged
merged 4 commits into from Mar 3, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Expand Up @@ -453,6 +453,32 @@ def log_load(post, loaded_time):
logging.debug('Loaded post {} at {}'.format(post['_id'], loaded_time))
```

#### Removing event handlers

Sometimes it is desirable to be able to remove event handlers from a Model (e.g. for testing purposes). Models expose a few methods to make this easy:

```python
BlogPost.on('did_save', log_save)
BlogPost.on('did_save', inc_save_count)
BlogPost.on('did_find', log_find)

# Inspect what handlers are registered for a given event
BlogPost.handlers('did_save') # => [<function log_save>, <function inc_save_count>]

# Remove a given handler
BlogPost.remove_handler('did_save', log_save)

# Remove all handlers registered against a given event
BlogPost.remove_all_handlers('did_save')

# Remove all handlers registered against a given list of events
BlogPost.remove_all_handlers('did_save', 'did_find')

# Remove all handlers registered all events
BlogPost.remove_all_handlers()

```


### Model State

Expand Down
43 changes: 37 additions & 6 deletions mongothon/events.py
@@ -1,26 +1,57 @@
class EventHandlerRegistrar(object):
"""Handles the registration of event handler functions against specific
"""
Handles the registration of event handler functions against specific
model events and the execution of those functions in response to
those events being emitted.

This class is internal to Mongothon and should not be manipulated
directly. Instead, consumer code should register and indirectly
invoke handlers via Models."""
invoke handlers via Models.
"""

def __init__(self):
self._handler_dict = {}

def register(self, event, fn):
"""Registers the given function as a handler to be applied
in response to the the given event."""
"""
Registers the given function as a handler to be applied
in response to the the given event.
"""

# TODO: Can we check the method signature?
self._handler_dict.setdefault(event, [])
if fn not in self._handler_dict[event]:
self._handler_dict[event].append(fn)

def apply(self, event, document, *args, **kwargs):
"""Applies all middleware functions registered against the given
event in order to the given document."""
"""
Applies all middleware functions registered against the given
event in order to the given document.
"""
for fn in self._handler_dict.get(event, []):
fn(document, *args, **kwargs)

def deregister(self, event, fn):
"""
Deregister the handler function from the given event.
"""
if event in self._handler_dict and fn in self._handler_dict[event]:
self._handler_dict[event].remove(fn)

def deregister_all(self, *events):
"""
Deregisters all handler functions, or those registered against the given event(s).
"""
if events:
for event in events:
self._handler_dict[event] = []
else:
self._handler_dict = {}

def handlers(self, event):
"""
Returns all handlers registered against the given event.
"""
if event in self._handler_dict:
return self._handler_dict[event]
return []
34 changes: 29 additions & 5 deletions mongothon/model.py
Expand Up @@ -69,11 +69,6 @@ def is_deleted(self):
"""Returns true if the model instance was deleted from the database."""
return self._state == Model.DELETED

def emit(self, event, *args, **kwargs):
"""Emits an event call to all handler functions registered against
this model's class and the given event type."""
self.handler_registrar.apply(event, self, *args, **kwargs)

def validate(self):
"""Validates this model against the schema with which is was constructed.
Throws a ValidationException if the document is found to be invalid."""
Expand Down Expand Up @@ -190,6 +185,35 @@ def register(fn):

return register

def emit(self, event, *args, **kwargs):
"""
Emits an event call to all handler functions registered against
this model's class and the given event type.
"""
self.handler_registrar.apply(event, self, *args, **kwargs)

@classmethod
def remove_handler(self, event, handler_func):
"""
Deregisters the given handler function from the given event on this Model.
When the given event is next emitted, the given function will not be called.
"""
self.handler_registrar.deregister(event, handler_func)

@classmethod
def remove_all_handlers(self, *events):
"""
Deregisters all handler functions, or those registered against the given event(s).
"""
self.handler_registrar.deregister_all(*events)

@classmethod
def handlers(self, event):
"""
Returns all handlers registered against the given event.
"""
return self.handler_registrar.handlers(event)

@classmethod
def class_method(cls, f):
"""Decorator which dynamically binds class methods to the model for later use."""
Expand Down
51 changes: 51 additions & 0 deletions tests/mongothon/events_test.py
Expand Up @@ -53,3 +53,54 @@ def test_double_registered_event_called_only_once(self):
self.registrar.apply('save', document)
handler.assert_called_once_with(document)

def test_handler_not_called_after_deregistration(self):
handler = Mock()
document = Mock()
arg = Mock()
kwarg = Mock()
self.registrar.register('save', handler)
self.registrar.deregister('save', handler)
self.registrar.apply('save', document, arg, kwarg=kwarg)
self.assertEquals(0, handler.call_count)

def test_deregister_when_not_registered(self):
handler = Mock()
self.registrar.register('save', handler)
self.registrar.deregister('save', handler)
self.registrar.deregister('save', handler)
self.registrar.deregister('bogus', handler)

def test_deregister_all(self):
handler1, handler2 = Mock(), Mock()
document = Mock()
arg = Mock()
kwarg = Mock()
self.registrar.register('save', handler1)
self.registrar.register('save', handler2)
self.registrar.deregister_all()
self.registrar.apply('save', document, arg, kwarg=kwarg)
self.assertEquals(0, handler1.call_count)
self.assertEquals(0, handler2.call_count)

def test_deregister_all_with_event_type(self):
handler1, handler2 = Mock(), Mock()
document = Mock()
arg = Mock()
kwarg = Mock()
self.registrar.register('save', handler1)
self.registrar.register('save', handler2)
self.registrar.deregister_all('reload')
self.registrar.apply('save', document, arg, kwarg=kwarg)
self.assertEquals(1, handler1.call_count)
self.assertEquals(1, handler2.call_count)
self.registrar.deregister_all('other', 'save')
self.assertEquals(1, handler1.call_count)
self.assertEquals(1, handler2.call_count)

def test_handlers(self):
handler1, handler2 = Mock(), Mock()
self.registrar.register('save', handler1)
self.registrar.register('save', handler2)
self.assertEquals([handler1, handler2], self.registrar.handlers('save'))
self.assertEquals([], self.registrar.handlers('other'))

22 changes: 22 additions & 0 deletions tests/mongothon/model_test.py
Expand Up @@ -70,6 +70,9 @@ def setUp(self):
self.Car = create_model(car_schema, self.mock_collection)
self.car = self.Car(doc)

def tearDown(self):
self.Car.remove_all_handlers()

def assert_predicates(self, model, is_new=False, is_persisted=False, is_deleted=False):
self.assertEquals(is_new, model.is_new())
self.assertEquals(is_persisted, model.is_persisted())
Expand Down Expand Up @@ -436,6 +439,25 @@ def test_emit_custom_event(self):
self.car.emit('fruit_explosion', 'apples', other_fruit='oranges')
handler.assert_called_once_with(self.car, 'apples', other_fruit='oranges')

def test_remove_handler(self):
handler = Mock()
self.Car.on('did_init', handler)
self.Car.remove_handler('did_init', handler)
self.Car()
self.assertEquals(0, handler.call_count)

def test_remove_all_handlers(self):
handler = Mock()
self.Car.on('did_init', handler)
self.Car.remove_all_handlers()
self.Car()
self.assertEquals(0, handler.call_count)

def test_handlers(self):
handler = Mock()
self.Car.on('did_init', handler)
self.assertEquals([handler], self.Car.handlers('did_init'))

def test_class_method_registration(self):
response = Mock()

Expand Down