<jupyter_start><jupyter_text>Watched Dictionaries---keeping track of key-value changes in a dict<jupyter_code># `watched` creates instance with a preregistered spectator.
from spectate import expose_as, watched, Bunch<jupyter_output><empty_output><jupyter_text>We use `expose_as` to create an `EventfulDict` that enables callbacks before and after an element of the dictionary is set. The function's signature requries the `name` of the new eventful type, a `base` class from which that new type should inherit, and the series of `methods` on that base class which can register a callback. In this case we enable callbacks for methods which can set or delete items:+ `__setitem__`+ `__delitem__`+ `setdefault`+ `popitem`.+ `pop`+ `update`<jupyter_code>EventfulDict = expose_as('EventfulDict', dict, '__setitem__',
    '__delitem__', 'setdefault', 'update', 'pop', 'popitem')
# a sentinel signifying an empty element
class Empty(object):
    def __repr__(self): return "Empty"
Empty = Empty()<jupyter_output><empty_output><jupyter_text>Using Closures--------------In the basic example we created beforebacks and afterbacks seperately when defining our eventful list. However it is also possible to define an afterback as closure inside a beforeback. This allows the afterback to capture the same information that would otherwise need to be passed to it from a beforeback inside its scope.Callbacks---------+ Have a signature of ``(instance, call)``    + ``instance`` is the owner of the method    + ``call`` is a ``Bunch`` with the keys:        + ``'name'`` - the name of the method which was called        + ``'args'`` - the arguments which that method will call        + ``'kwargs'`` - the keywords which that method will call+ Can ``return`` a closue which is called as an afterback.Closures--------+ Have a signature of ``(value)``    + ``'value'`` - the value returned by the method    + All other information is already contained in the closures scope.+ Should not ``return``<jupyter_code>def djoin(d1, d2):
    new = d1.copy()
    d1.update(d2)
    return new

def item_change(inst, key):
    old = inst.get(key, Empty)
    def printit(value):
        new = inst.get(key, Empty)
        if new != old:
            print("{%s: %s} -> {%s: %s}" %
                (key, old, key, new))
    return printit

def before_item_change(inst, call):
    return item_change(inst, call.args[0])

def before_popitem(inst, call):
    return item_change(inst, tuple(inst.keys())[0])

def before_update(inst, call):
    call_list = [item_change(inst, k) for k in
        set(call.args[0]) | set(call.kwargs)]
    def after_update(value):
        return [c(value) for c in call_list]
    return after_update

    
edict, spectator = watched(EventfulDict, {'a': 1, 'b':2})

methods = ('__setitem__', '__delitem__', 'pop', 'setdefault')
spectator.callback(methods, before_item_change)

spectator.callback('popitem', before_popitem)

spectator.callback('update', before_update)
# tests
edict['a'] = 2
edict.pop('a')
edict.popitem()
edict.setdefault('c', 3)
edict.update({'a':1, 'b':2})
del edict['c']
# won't notify
edict['a'] = 1<jupyter_output>{a: 1} -> {a: 2}
{a: 2} -> {a: Empty}
{b: 2} -> {b: Empty}
{c: Empty} -> {c: 3}
{a: Empty} -> {a: 1}
{b: Empty} -> {b: 2}
{c: 3} -> {c: Empty}
{'fraction_non_alphanumeric': 0.1405438435685915, 'fraction_numerical': 0.007027192178429576, 'mean_word_length': 4.131661442006269, 'pattern_counts': {'":': 0, '<': 12, '<?xml version=': 0, '>': 20, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '3', 'dir': 'jupyter-structured-clean-dedup', 'id': '264216', 'license': 'MIT', 'n_tokens_mistral': 1079, 'n_tokens_neox': 1042, 'n_words': 374, 'path': 'examples/advanced.ipynb', 'repo_name': 'draperjames/spectate', 'size': '3273'}