Skip to content

Commit

Permalink
Add proper functional API for adding/removing callbacks from callback…
Browse files Browse the repository at this point in the history
… container, and add support for item validators
  • Loading branch information
astrofrog committed Sep 17, 2024
1 parent 6b8d742 commit ec8cddb
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 17 deletions.
62 changes: 49 additions & 13 deletions echo/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@

class ContainerMixin:

def _setup_container(self):
self._callbacks = CallbackContainer()
self._item_validators = CallbackContainer()

def _prepare_add(self, value):
for validator in self._item_validators:
value = validator(value)

Check warning on line 16 in echo/containers.py

View check run for this annotation

Codecov / codecov/patch

echo/containers.py#L16

Added line #L16 was not covered by tests
if isinstance(value, list):
value = CallbackList(self.notify_all, value)
elif isinstance(value, dict):
Expand All @@ -22,7 +28,45 @@ def _cleanup_remove(self, value):
if isinstance(value, HasCallbackProperties):
value.remove_global_callback(self.notify_all)
elif isinstance(value, (CallbackList, CallbackDict)):
value.callbacks.remove(self.notify_all)
value.remove_callback(self.notify_all)

def add_callback(self, func, priority=0, validator=False):
"""
Add a callback to the container.
Note that validators are applied on a per item basis, whereas regular
callbacks are called with the whole list after modification.
Parameters
----------
func : func
The callback function to add
priority : int, optional
This can optionally be used to force a certain order of execution of
callbacks (larger values indicate a higher priority).
validator : bool, optional
Whether the callback is a validator, which is a special kind of
callback that gets called with the item being added to the
container *before* the container is modified. The validator can
return the value as-is, modify it, or emit warnings or an exception.
"""

if validator:
self._item_validator.append(func, priority=priority)

Check warning on line 55 in echo/containers.py

View check run for this annotation

Codecov / codecov/patch

echo/containers.py#L55

Added line #L55 was not covered by tests
else:
self._callbacks.append(func, priority=priority)

def remove_callback(self, func):
"""
Remove a callback from the container.
"""
for cb in (self._callbacks, self._item_validators):
if func in cb:
cb.remove(func)

def notify_all(self, *args, **kwargs):
for callback in self._callbacks:
callback(*args, **kwargs)


class CallbackList(list, ContainerMixin):
Expand All @@ -35,15 +79,11 @@ class CallbackList(list, ContainerMixin):

def __init__(self, callback, *args, **kwargs):
super(CallbackList, self).__init__(*args, **kwargs)
self.callbacks = CallbackContainer()
self.callbacks.append(callback)
self._setup_container()
self.add_callback(callback)
for index, value in enumerate(self):
super().__setitem__(index, self._prepare_add(value))

def notify_all(self, *args, **kwargs):
for callback in self.callbacks:
callback(*args, **kwargs)

def __repr__(self):
return "<CallbackList with {0} elements>".format(len(self))

Expand Down Expand Up @@ -113,15 +153,11 @@ class CallbackDict(dict, ContainerMixin):

def __init__(self, callback, *args, **kwargs):
super(CallbackDict, self).__init__(*args, **kwargs)
self.callbacks = CallbackContainer()
self.callbacks.append(callback)
self._setup_container()
self.add_callback(callback)
for key, value in self.items():
super().__setitem__(key, self._prepare_add(value))

def notify_all(self, *args, **kwargs):
for callback in self.callbacks:
callback(*args, **kwargs)

def clear(self):
for value in self.values():
self._cleanup_remove(value)
Expand Down
8 changes: 4 additions & 4 deletions echo/tests/test_containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ def test_list_additional_callbacks():
assert test1.call_count == 1
assert test2.call_count == 0

stub.prop1.callbacks.append(test2)
stub.prop1.add_callback(test2)

stub.prop2.append(5)
assert test1.call_count == 1
Expand All @@ -548,7 +548,7 @@ def test_list_additional_callbacks():
assert test1.call_count == 2
assert test2.call_count == 1

stub.prop1.callbacks.remove(test2)
stub.prop1.remove_callback(test2)
stub.prop1.append(4)
assert test1.call_count == 3
assert test2.call_count == 1
Expand All @@ -568,7 +568,7 @@ def test_dict_additional_callbacks():
assert test1.call_count == 1
assert test2.call_count == 0

stub.prop1.callbacks.append(test2)
stub.prop1.add_callback(test2)

stub.prop2['c'] = 3
assert test1.call_count == 1
Expand All @@ -578,7 +578,7 @@ def test_dict_additional_callbacks():
assert test1.call_count == 2
assert test2.call_count == 1

stub.prop1.callbacks.remove(test2)
stub.prop1.remove_callback(test2)
stub.prop1['e'] = 5
assert test1.call_count == 3
assert test2.call_count == 1

0 comments on commit ec8cddb

Please sign in to comment.