4. Extending spy

When the spy command runs, it will import all submodules of the spy_plugins namespace package. You can extend spy locally or from an installed package by installing a module there.

Specifically, you should create a directory named spy_plugins somewhere on Python’s module path (or ship one if you’re distributing an installable package) without an __init__.py containing a Python module that implements your extension.

There are two primary means of extending spy: adding fragment decorators, and adding functions to prelude.

4.1. Extending the prelude

Adding functions to the prelude is trivial: simply import spy.prelude and put things in it.

from spy import prelude
prelude.uc = lambda s: str(s).upper()
prelude.__all__ += ['uc']
$ spy -c uc <<< hello
HELLO

4.2. Adding decorators

spy’s decorators are created using a helper decorator, spy.decorators.decorator(), which does a lot of work to set up exception handling and deal with the optional context argument to fragments. Because of this, the form decorators must take is slightly prescribed. Basic usage is as follows:

@decorator('--uppercase', '-U', doc='Make the result uppercase')
def uppercase(fn, v, context):
    return str(fn(v, context)).upper()
$ spy -u pipe <<< hello
HELLO

In general, the function to which decorator() is applied to three arguments for the decorated fragment function, the input value and the context. fn is adjusted to always take the context argument for simplicity. The decorator function is responsible for calling fn and returning the result.

If it’s advantageous to do some setup first, it can be pulled into a function and passed as the prep keyword argument. Its return value will be passed as a fourth argument to the decorator.

def _prep_cached(fn):
    return {}

@decorator('--cached', '-C', doc='Cache this fragment', prep=_prep_cached)
def cached(fn, v, context, cache):
    if v not in cache:
        cache[v] = fn(v, context)
    return cache[v]
$ spy -m '[1,2,2,2,3,4]' -Cc print
1
2
3
4

Finally, if your decorator should take a literal string rather than a fragment, use the takes_string parameter. The decorator API is as above, except that the fragment function will return a tuple of its execution scope and the string.

@decorator('--template', '-t', doc='Template this string', takes_string=True)
def template(fn, v, context):
    env, s = fn(v, context)
    return string.Template(s).substitute(env)
$ spy '{"a": 10, "b": 20}' -kt '$a $b'
10 20