Skip to content

Commit

Permalink
Add emission_lock feature
Browse files Browse the repository at this point in the history
  • Loading branch information
nocarryr committed Feb 8, 2017
1 parent 53e232c commit 8a3283a
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 2 deletions.
45 changes: 43 additions & 2 deletions pydispatch/dispatch.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import types

from pydispatch.utils import WeakMethodContainer
from pydispatch.utils import WeakMethodContainer, EmissionHoldLock
from pydispatch.properties import Property


Expand All @@ -9,10 +9,11 @@ class Event(object):
This is used internally by :class:`Dispatcher`.
"""
__slots__ = ('name', 'listeners')
__slots__ = ('name', 'listeners', 'emission_lock')
def __init__(self, name):
self.name = name
self.listeners = WeakMethodContainer()
self.emission_lock = EmissionHoldLock(self)
def add_listener(self, callback):
self.listeners.add_method(callback)
def remove_listener(self, obj):
Expand All @@ -25,6 +26,9 @@ def __call__(self, *args, **kwargs):
Called by :meth:`~Dispatcher.emit`
"""
if self.emission_lock.held:
self.emission_lock.last_event = (args, kwargs)
return
for m in self.listeners.iter_methods():
r = m(*args, **kwargs)
if r is False:
Expand Down Expand Up @@ -154,3 +158,40 @@ def emit(self, name, *args, **kwargs):
if e is None:
e = self.__events[name]
return e(*args, **kwargs)
def emission_lock(self, name):
"""Holds emission of events and dispatches the last event on release
The context manager returned will store the last event data called by
:meth:`emit` and prevent callbacks until it exits. On exit, it will
dispatch the last event captured (if any)::
class Foo(Dispatcher):
_events_ = ['my_event']
def on_my_event(value):
print(value)
foo = Foo()
foo.bind(my_event=on_my_event)
with foo.emission_lock('my_event'):
foo.emit('my_event', 1)
foo.emit('my_event', 2)
>>> 2
Args:
name (str): The name of the :class:`Event` or :class:`Property`
Returns:
A context manager to be used by the ``with`` statement
Note:
The context manager is re-entrant, meaning that multiple calls to
this method within nested context scopes are possible.
"""
e = self.__property_events.get(name)
if e is None:
e = self.__events[name]
return e.emission_lock
24 changes: 24 additions & 0 deletions pydispatch/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,27 @@ def __init__(self, **kwargs):
self.data.del_callback = self._data_del_callback
def _data_del_callback(self, key):
self.del_callback(key)

class EmissionHoldLock(object):
def __init__(self, event_instance):
self.event_instance = event_instance
self.last_event = None
self.held = False
def acquire(self):
if self.held:
return
self.held = True
self.last_event = None
def release(self):
if not self.held:
return
if self.last_event is not None:
args, kwargs = self.last_event
self.last_event = None
self.held = False
self.event_instance(*args, **kwargs)
def __enter__(self):
self.acquire()
return self
def __exit__(self, *args):
self.release()
28 changes: 28 additions & 0 deletions tests/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,31 @@ def on_prop(*args, **kwargs):
assert len(props) == 1

sender.unbind(on_event, on_prop)

def test_emission_lock(listener, sender):
sender.register_event('on_test')
sender.bind(on_test=listener.on_event)

letters = 'abcdefghijkl'

sender.emit('on_test', letters[0], emit_count=0)
assert len(listener.received_event_data) == 1
listener.received_event_data = []

with sender.emission_lock('on_test'):
for i in range(len(letters)):
sender.emit('on_test', letters[i], emit_count=i)

assert len(listener.received_event_data) == 1
e = listener.received_event_data[0]
assert e['args'] == (letters[i], )
assert e['kwargs']['emit_count'] == i

listener.received_event_data = []

with sender.emission_lock('on_test'):
sender.emit('on_test', 'outer')
with sender.emission_lock('on_test'):
sender.emit('on_test', 'inner')
assert len(listener.received_event_data) == 1
assert listener.received_event_data[0]['args'] == ('inner', )
78 changes: 78 additions & 0 deletions tests/test_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,84 @@ def on_test_list(self, *args, **kwargs):

assert a.received == ['test_prop', 'test_dict', 'test_list']

def test_emission_lock(listener):
from pydispatch import Dispatcher, Property
from pydispatch.properties import ListProperty, DictProperty

class A(Dispatcher):
test_prop = Property()
test_dict = DictProperty()
test_list = ListProperty()

a = A()
a.bind(test_prop=listener.on_prop, test_list=listener.on_prop, test_dict=listener.on_prop)

letters = 'abcdefghijkl'

a.test_prop = 'foo'
a.test_list = [-1] * 4
a.test_dict = {'a':0, 'b':1, 'c':2, 'd':3}

assert len(listener.property_events) == 3
listener.property_events = []
listener.property_event_kwargs = []

with a.emission_lock('test_prop'):
for i in range(4):
a.test_prop = i
assert len(listener.property_events) == 1
assert listener.property_event_kwargs[0]['property'].name == 'test_prop'
assert listener.property_events[0] == i

listener.property_events = []
listener.property_event_kwargs = []

with a.emission_lock('test_list'):
a.test_prop = 'foo'
for i in range(4):
a.test_list = [i] * 4
assert len(listener.property_events) == 2
assert listener.property_event_kwargs[0]['property'].name == 'test_prop'
assert listener.property_events[0] == 'foo'
assert listener.property_event_kwargs[1]['property'].name == 'test_list'
assert listener.property_events[1] == [i] * 4

listener.property_events = []
listener.property_event_kwargs = []

with a.emission_lock('test_dict'):
a.test_prop = 'bar'
a.test_list[0] = 'a'
for i in range(4):
for key in a.test_dict.keys():
a.test_dict[key] = i
assert len(listener.property_events) == 3
assert listener.property_event_kwargs[0]['property'].name == 'test_prop'
assert listener.property_events[0] == 'bar'
assert listener.property_event_kwargs[1]['property'].name == 'test_list'
assert listener.property_events[1][0] == 'a'
assert listener.property_event_kwargs[2]['property'].name == 'test_dict'
assert listener.property_events[2] == {k:i for k in a.test_dict.keys()}

listener.property_events = []
listener.property_event_kwargs = []

with a.emission_lock('test_prop'):
with a.emission_lock('test_list'):
with a.emission_lock('test_dict'):
for i in range(4):
a.test_prop = i
a.test_list[0] = i
a.test_dict[i] = 'foo'
assert len(listener.property_events) == 3
assert listener.property_event_kwargs[0]['property'].name == 'test_dict'
for k in range(4):
assert listener.property_events[0][k] == 'foo'
assert listener.property_event_kwargs[1]['property'].name == 'test_list'
assert listener.property_events[1][0] == i
assert listener.property_event_kwargs[2]['property'].name == 'test_prop'
assert listener.property_events[2] == i

def test_copy_on_change(listener):
from pydispatch import Dispatcher
from pydispatch.properties import ListProperty, DictProperty
Expand Down

0 comments on commit 8a3283a

Please sign in to comment.