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.