Spectating Other Types

In a prior example demonstrating how to create a custom model we used notifier() to produce events. This is sufficient in most cases, but sometimes you aren’t able to manually trigger events from within a method. This might occur when inheriting from a builtin type (e.g. list, dict, etc) that is implemented in C or a third party package that doesn’t use spectate. In those cases, you must wrap an existing method and are religated to producing events before and/or after it gets called.

In these scenarios you must define a Model subclass which has Control objects assigned to it. Each control object is responsible for observing calls to particular methods on the model class. For example, if you wanted to know when an element was appended to a list you might observe the append method.

To show how this works we will implement a simple counter with the goal of knowing when the value in the counter has incremented or decremented. To get started we should create a Counter class which inherits from Model and define its increment and decrement methods normally:

Note

Usually if you’re using Control objects you’d do it with multiple inheritance, but to keep things simple we aren’t doing that in the following examples.

from spectate import mvc

class Counter(mvc.Model):

    def __init__(self):
        self.value = 0

    def increment(self, amount):
        self.value += amount

    def decrement(self, amount):
        self.value -= amount
c = Counter()
c.increment(1)
c.increment(1)
c.decrement(1)
assert c.value == 1

Adding Model Controls

Because we know that the value within the Counter changes whenever increment or decrement is called these are the methods that we must observe in order to determine whether, and by how much it changes. Do do this we should add a Control to the Counter and pass in the names of the methods it should be tracking.

from spectate import mvc

class Counter(mvc.Model):

    def __init__(self):
        self.value = 0

    def increment(self, amount):
        self.value += amount

    def decrement(self, amount):
        self.value -= amount

    _control_change = mvc.Control('increment', 'decrement')

We define the behavior of _control_change with methods that are triggered before and/or after the ones being observed. We register these with Control.before() and Control.after(). For now our beforeback and afterback will just contain print statements so we can see what they receive when they are called.

from spectate import mvc

class Counter(mvc.Model):

    def __init__(self):
        self.value = 0

    def increment(self, amount):
        self.value += amount

    def decrement(self, amount):
        self.value -= amount

    _control_change = mvc.Control(
        ["increment", "decrement"],
        before="_before_change",
        after="_after_change",
    )

    def _before_change(self, call, notify):
        print("BEFORE")
        print(call)
        print(notify)
        print()
        return "result-from-before"

    def _after_change(self, answer, notify):
        print("AFTER")
        print(answer)
        print(notify)
        print()

No lets see what happens we can call increment or decrement:

c = Counter()
c.increment(1)
c.decrement(1)
BEFORE
{'name': 'increment', 'kwargs': {}, 'args': (1,), 'parameters': <function BoundControl.before.<locals>.beforeback.<locals>.parameters at 0x7f9ce57e8a60>}
<function BoundControl.before.<locals>.beforeback.<locals>.notify at 0x7f9ce57e89d8>

AFTER
{'before': 'result-from-before', 'name': 'increment'}
<function BoundControl.after.<locals>.afterback.<locals>.notify at 0x7f9ce57e89d8>

BEFORE
{'name': 'decrement', 'kwargs': {}, 'args': (1,), 'parameters': <function BoundControl.before.<locals>.beforeback.<locals>.parameters at 0x7f9ce57f2400>}
<function BoundControl.before.<locals>.beforeback.<locals>.notify at 0x7f9ce57e89d8>

AFTER
{'before': 'result-from-before', 'name': 'decrement'}
<function BoundControl.after.<locals>.afterback.<locals>.notify at 0x7f9ce57e89d8>

Control Callbacks

The callback pair we registered to our Counter when learning how to define controls, hereafter referred to as “beforebacks” and “afterbacks” are how event information is communicated to views. Defining both a beforeback and an afterback is not required, but doing so allows for a beforeback to pass data to its corresponding afterback which in turn makes it possible to compute the difference between the state before and the state after a change takes place:

from spectate import mvc

class Counter(mvc.Model):

    def __init__(self):
        self.value = 0

    def increment(self, amount):
        self.value += amount

    def decrement(self, amount):
        self.value -= amount

    _control_change = mvc.Control(
        ["increment", "decrement"],
        before="_before_change",
        after="_after_change",
    )

    def _before_change(self, call, notify):
        amount = call.parameters()["amount"]
        print(f"value will {call['name']} by {amount}")
        old_value = self.value
        return old_value

    def _after_change(self, answer, notify):
        old_value = answer["before"]  # this was returned by `_before_change`
        new_value = self.value
        print(f"the old value was {old_value})
        print(f"the new value is {new_value})
        print(f"the value changed by {new_value - old_value}")

Now we can try incrementing and decrementing as before:

c = Counter()
c.increment(1)
c.decrement(1)
value will increment by 1
the old value was 0
the new value is 1
the value changed by 1
value will decrement by 1
the old value was 1
the new value is 0
the value changed by -1

Control Event Notifications

We’re now able to use “beforebacks” and “afterbacks” to print out information about a model before and after a change occures, but what we actually want is to send this same information to views as we did when we learned The Basics. To accomplish this we use the notify function passed into the beforeback and afterback and pass it keyword parameters that can be consumed by views. To keep things simple we’ll just replace our print statements with calls to notify:

from spectate import mvc

class Counter(mvc.Model):

    def __init__(self):
        self.value = 0

    def increment(self, amount):
        self.value += amount

    def decrement(self, amount):
        self.value -= amount

    _control_change = (
        mvc.Control('increment', 'decrement')
        .before("_before_change")
        .after("_after_change")
    )

    def _before_change(self, call, notify):
        amount = call.parameters()["amount"]
        notify(message="value will %s by %s" % (call["name"], amount))
        old_value = self.value
        return old_value

    def _after_change(self, answer, notify):
        old_value = answer["before"]  # this was returned by `_before_change`
        new_value = self.value
        notify(message="the old value was %r" % old_value)
        notify(message="the new value is %r" % new_value)
        notify(message="the value changed by %r" % (new_value - old_value))

To print out the same messages as before we’ll need to register a view with out counter:

c = Counter()

@mvc.view(c)
def print_messages(c, events):
    for e in events:
        print(e["message"])

c.increment(1)
c.decrement(1)
value will increment by 1
the old value was 0
the new value is 1
the value changed by 1
value will decrement by 1
the old value was 1
the new value is 0
the value changed by -1

Control Beforebacks

Have a signature of (call, notify) -> before

  • call is a dict with the keys

    • 'name' - the name of the method which was called

    • 'args' - the arguments which that method will call

    • 'kwargs' - the keywords which tCallbacks are registered to specific methods in pairs - one will be triggered before, and the other after, a call to that method is made. These two callbacks are referred to as “beforebacks” and “afterbacks” respectively. Defining both a beforeback and an afterback in each pair is not required, but doing so allows a beforeback to pass data to its corresponding afterback.

    • parameters a function which returns a dictionary where the args and kwargs passed to the method have been mapped to argument names. This won’t work for builtin method like dict.get() since they’re implemented in C.

  • notify is a function which will distribute an event to views

  • before is a value which gets passed on to its respective afterback.

Control Afterbacks

Have a signature of (answer, notify)

  • answer is a dict with the keys

    • 'name' - the name of the method which was called

    • 'value' - the value returned by the method

    • 'before' - the value returned by the respective beforeback

  • notify is a function which will distribute an event to views