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)