Function compositing

One of Python’s strengths is how easy it is to manipulate functions and combine them. However, this typically breaks tools such as Clize which try to inspect the resulting callable and only get vague information. Fortunately, using the functions found in sigtools, we can overcome this drawback.

Let’s look at how you can create decorators that work well with Clize.

Creating decorators is useful if you want to share behaviour across multiple functions passed to run, such as extra parameters or input/output formatting.

Using a decorator to add new parameters and modify the return value

Let’s create a decorator that transforms the output of the wrapped function when passed a specific flag.

from sigtools.modifiers import autokwoargs
from sigtools.wrappers import wrapper_decorator


@wrapper_decorator
@autokwoargs
def with_uppercase(wrapped, uppercase=False, *args, **kwargs):
    """
    Formatting options:

    uppercase: Print output in capitals
    """
    ret = wrapped(*args, **kwargs)
    if uppercase:
        return str(ret).upper()
    else:
        return ret

wrapper_decorator lets our with_uppercase function decorate other functions:

from clize import run


@with_uppercase
def hello_world(name=None):
    """Says hello world

    name: Who to say hello to
    """
    if name is not None:
        return 'Hello ' + name
    else:
        return 'Hello world!'


if __name__ == '__main__':
    run(hello_world)

Each time the decorated function is run, with_uppercase will be run with the decorated function as first argument wrapped.

wrapper_decorator will tell Clize that the combined function has the same signature as:

@kwoargs('uppercase')
def hello_world(name=None, uppercase=False):
    pass

This is the signature you would get by “putting” the parameters of the decorated function in place of the wrapper’s *args, **kwargs. It is what wrapper_decorator expects it to do, but that can be changed.

With the correct signature advertised, the command-line interface matches it:

$ python examples/decorators.py --uppercase
HELLO WORLD!
$ python examples/decorators.py john
Hello john
$ python examples/decorators.py john --uppercase
HELLO JOHN

The help system will also pick up on the fact that the function is decorated and will read parameter descriptions from the decorator’s docstring:

$ python decorators.py --help
Usage: decorators.py [OPTIONS] [name]

Says hello world

Positional arguments:
  name          Who to say hello to

Formatting options:
  --uppercase   Print output in capitals

Other actions:
  -h, --help    Show the help

Providing an argument using a decorator

When you’re passing new arguments to the wrapped function in addition to *args, **kwargs, things get a little more complicated. You have to tell Clize that some of the wrapped function’s parameters shouldn’t appear in place of the wrapper’s *args, **kwargs. call wrapper_decorator with the number of positional arguments you insert before *args, then the names of each named argument that you pass to the wrapped function.

See also

Picking the appropriate arguments for forwards

The arguments for wrapper_decorator are the same as in sigtools.specifiers.forwards, so you may use this section for further information.

from sigtools.modifiers import autokwoargs
from sigtools.wrappers import wrapper_decorator


def get_branch_object(repository, branch_name):
    return repository, branch_name


@wrapper_decorator(0, 'branch')
@autokwoargs
def with_branch(wrapped,
            repository='.', branch='master',
            *args, **kwargs):
    """Decorate with this so your function receives a branch object

    repository: A directory belonging to the repository to operate on

    branch: The name of the branch to operate on
    """
    return wrapped(
        *args, branch=get_branch_object(repository, branch), **kwargs)

Here we pass 0, 'branch' to wrapper_decorator because we call wrapped with no positional arguments besides *args, and branch as named argument.

You can then use the decorator like before:

from clize import run


@with_branch
@autokwoargs
def diff(branch=None):
    """Show the differences between the committed code and the working tree."""
    return "I'm different."


@with_branch
@autokwoargs
def commit(branch=None, *text):
    """Commit the changes.

    text: A message to store alongside the commit
    """
    return "All saved.: " + ' '.join(text)


@with_branch
@autokwoargs
def revert(branch=None):
    """Revert the changes made in the working tree."""
    return "There is no chip, John."


run(diff, commit, revert,
    description="A mockup version control system(like git, hg or bzr)")

Using a composed function to process arguments to a parameter

You can use clize.parameters.argument_decorator to have a second function process an argument while still being able to use parameters of its own:

from clize import run
from clize.parameters import argument_decorator


@argument_decorator
def read_server(arg, *, port=80, _6=False):
    """
    Options for {param}:

    port: Which port to connect on

    _6: Use IPv6?
    """
    return (arg, port, _6)


def get_page(server:read_server, path):
    """
    server: The server to contact

    path: The path of the resource to fetch
    """
    print("Connecting to", server, "to get", path)


run(get_page)

read_server‘s parameters will be available on the CLI. When a value is read that would feed the server parameter, read_server is called with it and its collected arguments. Its return value is then used as the server parameter of get_page:

$ python argdeco.py --help
Usage: argdeco.py [OPTIONS] [--port=INT] [-6] server path

Arguments:
  server       The server to contact
  path         The path of the resource to fetch

Options for server:
  --port=INT   Which port to connect on (default: 80)
  -6           Use IPv6?

Other actions:
  -h, --help   Show the help

A few notes:

  • Besides arg which receives the original value, you can only use named parameters
  • The decorator’s docstring is used to document its parameters. It can be preferrable to use a section in order to distinguish them from other parameters.
  • Appearances of {param} in the docstring are replaced with the parameter’s name.

You can also use this on named parameters as well as on *args, but the names of the composited parameters must not conflict with other parameters:

from clize import run
from clize.parameters import argument_decorator


@argument_decorator
def read_server(arg, *, port=80, _6=False):
    """
    Options for {param}:

    port: Which port to connect on

    _6: Use IPv6?
    """
    return (arg, port, _6)


def get_page(path, *servers:read_server):
    """
    server: The server to contact

    path: The path of the resource to fetch
    """
    print("Connecting to", servers, "to get", path)


run(get_page)
$ python argdeco.py --help
Usage: argdeco.py [OPTIONS] path [[--port=INT] [-6] servers...]

Arguments:
  path         The path of the resource to fetch
  servers...

Options for servers:
  --port=INT   Which port to connect on (default: 80)
  -6           Use IPv6?

Other actions:
  -h, --help   Show the help
$ python argdeco.py -6 abc
argdeco.py: Missing required arguments: servers
Usage: argdeco.py [OPTIONS] path [[--port=INT] [-6] servers...]
$ python argdeco.py /eggs -6 abc
Connecting to (('abc', 80, True),) to get /eggs
$ python argdeco.py /eggs -6 abc def
Connecting to (('abc', 80, True), ('def', 80, False)) to get /eggs
$ python argdeco.py /eggs -6 abc def --port 8080 cheese
Connecting to (('abc', 80, True), ('def', 80, False), ('cheese', 8080, False)) to get /eggs

Congratulations, you’ve reached the end of the tutorials! You can check out the parameter reference or see how you can extend the parser.