Handling Events¶
Spectate provides a series of context managers which allow you to capture and then modify events before they are distributed to views. This allows you to hold, rollback, and even mute events. These context managers are useful for handling edge cases in your code, improving performance by merging events, or undo unwanted changes.
Holding Events¶
It’s often useful to withhold sending notifications until all your changes are complete.
Using the hold()
context manager, events created when
modifying a model won’t be distributed until we exit the context:
d = mvc.Dict()
# effectively the same as the printer view above
mvc.view(d, lambda d, e: list(map(print, e)))
print("before")
with mvc.hold(d):
d["a"] = 1
print("during")
# notifications are sent upon exiting
print("after")
before
during
{'key': 'a', 'old': Undefined, 'new': 1}
after
Merging Events¶
Sometimes there is a block of code in which it’s possible to produce duplicate events
or events which could be merged into one. By passing in a reducer
to
hold()
you can change the list of events just before they
are distributed. This is done by having the reducer
return or yield the new events.
from spectate import mvc
d = mvc.Dict()
mvc.view(d, lambda _, es: list(map(print, es)))
def merge_dict_events(model, events):
changes = {}
for e in events:
if e.key in changes:
changes[e.key][1] = e.new
else:
changes[e.key] = [e.old, e.new]
for key, (old, new) in changes.items():
yield {"key": key, "new": new, "old": old}
with mvc.hold(d, reducer=merge_dict_events):
for i in range(5):
# this loop would normally produce 5 different events
d["a"] = i
{'key': 'a', 'new': Undefined, 'old': 4}
Rolling Back Events¶
When an error occurs while modifying a model you may not want to distribute events.
Using rollback()
you can suppress events that were produced
in the same context as an error:
from spectate import mvc
d = mvc.Dict()
@mvc.view(d)
def should_not_be_called(d, events):
# we never call this view
assert False
try:
with mvc.rollback(d):
d["a"] = 1
d["b"] # key doesn't exist
except KeyError:
pass
Rolling Back Changes¶
Suppressing events after an error may not be enough. You can pass rollback()
an undo
function which gives you a chances to analyze the events in order to determine
and then return a model to its original state. Any events that you might produce while
modifying a model within the undo
function will be muted.
d = mvc.Dict()
def undo_dict_changes(model, events, error):
seen = set()
for e in reversed(events):
if e.old is mvc.Undefined:
del model[e.key]
else:
model[e.key] = e.old
try:
with mvc.rollback(d, undo=undo_dict_changes):
d["a"] = 1
d["b"] = 2
print(d)
d["c"]
except KeyError:
pass
print(d)
{'a': 1, 'b': 2}
{}
Muting Events¶
If you are setting a default state, or returning to one, it may be useful to withhold
events completely. This one’s pretty simple compared to the context managers above.
Just use mute()
and within its context, no events will
be distributed:
from spectate import mvc
l = mvc.List()
@mvc.view(l)
def raises(events):
# this won't ever happen
raise ValueError("Events occured!")
with mvc.mute(l):
l.append(1)
Manually Notifying¶
At times, and more likely when writing tests, you may need to forcefully send an event
to a model. This can be achieved using the notifier()
context
manager which provides a notify()
function identical to the one seen in
Control Callbacks.
Warning
While you could use notifier()
instead of adding
Adding Model Controls to your custom models, this is generall discouraged
because the resulting implementation is resistent to extension in subclasses.
from spectate import mvc
m = mvc.Model()
@mvc.view(m)
def printer(m, events):
for e in events:
print(e)
with mvc.notifier(m) as notify:
# the view should print out this event
notify(x=1, y=2)