Function compositing¶
One of Python’s strengths is how easy it is to manipulate functions and combine them. However, this often breaks tools which rely on introspection to function.
This isn’t the case with Clize, which uses sigtools
to understand how your
functions expect to be called.
Let’s write some decorators and see how they integrate 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.wrappers import decorator
@decorator
def with_uppercase(wrapped, *args, uppercase=False, **kwargs):
"""
Formatting options:
:param uppercase: Print output in capitals
"""
ret = wrapped(*args, **kwargs)
if uppercase:
return str(ret).upper()
else:
return ret
decorator
lets our with_uppercase
function decorate other functions:
from clize import run
@with_uppercase
def hello_world(name=None):
"""Says hello world
:param name: Who to say hello to
"""
if name is not None:
return 'Hello ' + name
else:
return 'Hello world!'
if __name__ == '__main__':
run(hello_world)
Every time hello_world
is called, with_uppercase
will be called with
the decorated function as first argument (wrapped
).
Note
sigtools.wrappers.decorator
is used here to create decorators. It offers
a simple and convenient way of creating decorators in a reliable way.
However, you don’t need to use it to make use of decorators with Clize and you may use other means of creating decorators if you wish.
Clize will treat hello_world
as if it had the same signature as:
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
.
When you run this function, the CLI parameters will automatically match the combined signature:
$ python3 examples/decorators.py --uppercase
HELLO WORLD!
$ python3 examples/decorators.py john
Hello john
$ python3 examples/decorators.py john --uppercase
HELLO JOHN
The help system will also adapt 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¶
You can also provide the decorated function with additional arguments much in the same way.
from sigtools.wrappers import decorator
def get_branch_object(repository, branch_name):
return repository, branch_name
@decorator
def with_branch(wrapped, *args, repository='.', branch='master', **kwargs):
"""Decorate with this so your function receives a branch object
:param repository: A directory belonging to the repository to operate on
:param branch: The name of the branch to operate on
"""
return wrapped(*args, branch=get_branch_object(repository, branch), **kwargs)
Simply provide an additional argument to the wrapped function. It will automatically be skipped during argument parsing and will be omitted from the help.
You can apply the decorator like before, with each decorated function receiving
the branch
argument as supplied by the decorator.
from clize import run
@with_branch
def diff(*, branch=None):
"""Show the differences between the committed code and the working tree."""
return "I'm different."
@with_branch
def commit(*text, branch=None):
"""Commit the changes.
:param text: A message to store alongside the commit
"""
return "All saved.: " + ' '.join(text)
@with_branch
def revert(*, branch=None):
"""Revert the changes made in the working tree."""
return "All changes reverted!"
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 separate function
process an argument while adding parameters of its own. It’s like having a
mini argument parser just for one argument:
from clize import run
from clize.parameters import argument_decorator
@argument_decorator
def read_server(arg, *, port=80, _6=False):
"""
Options for {param}:
:parser port: Which port to connect on
:parser _6: Use IPv6?
"""
return (arg, port, _6)
def get_page(server:read_server, path):
"""
:parser server: The server to contact
:parser 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 keyword-only parametersThe decorator’s docstring is used to document its parameters. It can be preferable 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.Parameter names must not conflict with other parameters.
You can also use this on named parameters, but this gets especially interesting
on *args
parameters, as each argument meant for it can have its own options:
from clize import run
from clize.parameters import argument_decorator
@argument_decorator
def read_server(arg, *, port=80, _6=False):
"""
Options for {param}:
:param port: Which port to connect on
:param _6: Use IPv6?
"""
return (arg, port, _6)
def get_page(path, *servers:read_server):
"""
:param server: The server to contact
:param 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.
If you’re stuck, need help or simply wish to give feedback you can chat using your GitHub or Twitter handle on Gitter.