Commands and Groups in Python Click














































Commands and Groups in Python Click



Commands and Groups

The capability of arbitrarily nesting command line utilities is the most important feature
of Click.This is implemented through the Command and Group (actually MultiCommand).

Command Vs MultiCommand(Group):
For a regular command, the callback is executed whenever the command runs. If the script
is the only command, it will always fire (unless a parameter callback prevents it. This 
for instance happens if someone passes --help to the script).For groups and multi commands,
the callback fires whenever a subcommand fires (unless this behaviour is changed). What
this means in practice is that an outer command runs when an inner command runs. To
create a group of commands(cli and sync) ,we write the following script and name it
tool.py:
import click

@click.group()
@click.option('--debug/--no-debug', default=False)
def cli(debug):
    click.echo('Debug mode is %s' % ('on' if debug else 'off'))

@cli.command()  # @cli, not @click!
def sync():
    click.echo('Syncing')

You will need to edit the  setup.py file and then test the script:
$mytool sync
Debug mode is off
Syncing


Passing Parameters:
Click strictly separates parameters between commands and subcommands. What this means is 
that options and arguments for a specific command have to be specified after the command 
name itself, but before any other command names.This behavior is already observable with
the predefined --help option. Suppose we have a program called tool.py, containing a 
subcommand called sub.
mytool --help will return the help for the whole program (listing subcommands).

mytool sync --help will return the help for the sub subcommand.

But mytool --help sync will treat --help as an argument for the main program. Click then
invokes the callback for --help, which prints the help and aborts the program before click
can process the subcommand.
Nested Commands:

In the example above, the basic command group accepts a debug argument which is passed to
its callback, but not to the sync command itself. The sync command only accepts its own 
arguments.This allows tools.py to act completely independent of each other, but  one 
command talks to a nested one through an object called  Context.Each time a command is 
invoked, a new context is created and linked with the parent context. Normally, you can%u2019t
see these contexts, but they are there. Contexts are passed to parameter callbacks 
together with the value automatically. Commands can also ask for the context to be passed
by marking themselves with the pass_context() decorator. In that case, the context is 
passed as first argument.
The context can also carry a program specified object that can be used for the program%u2019s
purposes. What this means is ,like in the script below, we use the context object to modify
or use  the options passed to the command in parameters like debug and name:

import click
@click.group()
@click.option('--debug/--no-debug', default=False)
@click.option('--name', default=None)
@click.pass_context
def cli(ctx, debug,name):
    # ensure that ctx.obj exists and is a dict (in case `cli()` is called
    # by means other than the `if` block below
    ctx.ensure_object(dict)

    ctx.obj['DEBUG'] = debug
    ctx.obj['NAME'] = name

    click.echo('Inside cli Debug is %s' % (ctx.obj['DEBUG'] and 'on' or 'off'))
    click.echo('Inside cli Name is %s' % (ctx.obj['NAME']))


@cli.command()
@click.pass_context
def sync(ctx):
    ctx.obj['NAME']='Ranjit'
    ctx.obj['DEBUG']='on'
    click.echo('Inside sync with Debug is %s' % (ctx.obj['DEBUG'] and 'on' or 'off'))
    click.echo('Inside sync Name is %s' % (ctx.obj['NAME']))

if __name__ == '__main__':
    cli(obj={})

To test this :
$ mytool --name=Tony sync
Inside cli Debug is off
Inside cli Name is Tony
Inside sync with Debug is on
Inside sync Name is Ranjit


Group Invocation Without Command
By default, a group or multi command is not invoked unless a subcommand is passed. In fact,
not providing a command automatically passes --help by default. This behaviour can be 
changed by passing invoke_without_command=True to a group. In that case, the callback is
always invoked instead of showing the help page. The context object also includes 
information about whether or not the invocation would go to a subcommand.
@click.group(invoke_without_command=True)
@click.pass_context
def cli(ctx):
    if ctx.invoked_subcommand is None:
        click.echo('I was invoked without subcommand')
    else:
        click.echo('I am about to invoke %s' % ctx.invoked_subcommand)

@cli.command()
def sync():
    click.echo('The subcommand')

The command:

$ tool
I was invoked without subcommand
$ tool sync
I am about to invoke sync
The subcommand

Custom Multi Commands:
In addition to using click.group(), you can also build your own custom multi commands. 
This is useful when you want to support commands being loaded lazily from plugins.A custom 
multi command just needs to implement a list and load method like in the example below.We
have a folder named commands that holds various scripts for commands that are called when 
the following script is called with that argument:

import click
import os

plugin_folder = os.path.join(os.path.dirname(__file__), 'commands')

class MyCLI(click.MultiCommand):

    def list_commands(self, ctx):
        rv = []
        for filename in os.listdir(plugin_folder):
            if filename.endswith('.py'):
                rv.append(filename[:-3])
        rv.sort()
        return rv

    def get_command(self, ctx, name):
        ns = {}
        fn = os.path.join(plugin_folder, name + '.py')
        with open(fn) as f:
            code = compile(f.read(), fn, 'exec')
            eval(code, ns, ns)
        return ns['cli']

cli = MyCLI(help='This tool\'s subcommands are loaded from a '
            'plugin folder dynamically.')

if __name__ == '__main__':
    cli()
To test the above script, first we call the help to see the command scripts available:
$ mytool 
Usage: mytool [OPTIONS] COMMAND [ARGS]...

  This tool's subcommands are loaded from a plugin folder dynamically.

Options:
  --help  Show this message and exit.

Commands:
  hello
Then we call the command script ('hello' in the case above):

$ mytool hello 
Usage: mytool hello [OPTIONS] COMMAND [ARGS]...

Options:
  --debug / --no-debug
  --help                Show this message and exit.

Commands:
  sync

Then we call the sub-command('sync' in the case above) :
$ mytool hello sync
Debug mode is off
Syncing

Multi Command Pipelines
A very common usecase of multi command chaining is to have one command process the result
of the previous command. There are various ways in which this can be facilitated. The most 
obvious way is to store a value on the context object and process it from function to 
function. This works by decorating a function with pass_context() after which the context 
object is provided and a subcommand can store its data there.
Another way to accomplish this is to setup pipelines by returning processing functions. 
Think of it like this: when a subcommand gets invoked it processes all of its parameters 
and comes up with a plan of how to do its processing. At that point it then returns a 
function and returns.
Where do the returned functions go? The chained multicommand can register a callback with
MultiCommand.resultcallback() that goes over all these functions and then invoke them.

Example:
@click.group(chain=True, invoke_without_command=True)
@click.option('-i', '--input', type=click.File('r'))
def cli(input):
    pass

@cli.resultcallback()
def process_pipeline(processors, input):
    iterator = (x.rstrip('\r\n') for x in input)
    for processor in processors:
        iterator = processor(iterator)
    for item in iterator:
        click.echo(item)

@cli.command('uppercase')
def make_uppercase():
    def processor(iterator):
        for line in iterator:
            yield line.upper()
    return processor

@cli.command('lowercase')
def make_lowercase():
    def processor(iterator):
        for line in iterator:
            yield line.lower()
    return processor

@cli.command('strip')
def make_strip():
    def processor(iterator):
        for line in iterator:
            yield line.strip()
    return processor
The code above has these features:
1.First we create a group that can be chainable and we set Click to invoke without any 
  subcommands. If this would not be done, then invoking an empty pipeline would produce 
  the help page instead of running the result callbacks.
2.Then we  register a result callback on our group. This callback will be invoked with an 
  argument which is the list of all return values of all subcommands and then the same 
  keyword parameters as our group itself. This means we can access the input file easily 
  there without having to use the context object.
3.In this result callback we create an iterator of all the lines in the input file and then
  pass this iterator through all the returned callbacks from all subcommands and finally 
  we print all lines to stdout.
4.After that point we can register as many subcommands as we want and each subcommand can 
  return a processor function to modify the stream of lines.

To test it, you can pass the commands like this ,the order doesn't matter:
$ mytool -i input.txt lowercase uppercase strip
THIS IS INPUT
HELLO THERE

One important thing of note is that Click shuts down the context after each callback has 
been run. This means that for instance file types cannot be accessed in the processor 
functions as the files will already be closed there. This limitation is unlikely to change
because it would make resource handling much more complicated. For such it%u2019s recommended 
to not use the file type and manually open the file through open_file().

Overriding Defaults
By default, the default value for a parameter is pulled from the default flag that is 
provided when it%u2019s defined, but that%u2019s not the only place defaults can be loaded from. 
The other place is the Context.default_map (a dictionary) on the context. This allows 
defaults to be loaded from a configuration file to override the regular defaults.This is 
useful if you plug in some commands from another package but you%u2019re not satisfied with the
defaults.

Context Defaults
Starting with Click 2.0 you can override defaults for contexts not just when calling your 
script, but also in the decorator that declares a command. For instance given the previous
example which defines a custom default_map this can also be accomplished in the decorator 
now.
Example:

CONTEXT_SETTINGS = dict(
    default_map={'runserver': {'port': 5000}}

In the case above, the runserver command can be used to modify the default port number 
passed as an option.

Command Return Values
In essence any command callback can now return a value. This return value is bubbled to 
certain receivers.
Things to know about return values:
--The return value of a command callback is generally returned from the BaseCommand.invoke()
   method. The exception to this rule has to do with Groups:
   In a group the return value is generally the return value of the subcommand invoked. 
   The only exception to this rule is that the return value is the return value of the 
   group callback if it%u2019s invoked without arguments and invoke_without_command is enabled.

   If a group is set up for chaining then the return value is a list of all subcommands%u2019 
   results.

   Return values of groups can be processed through a MultiCommand.result_callback. This is
   invoked with the list of all return values in chain mode, or the single return value in
   case of non chained commands.

--The return value is bubbled through from the Context.invoke() and Context.forward() 
  methods. This is useful in situations where you internally want to call into another
  command.

--Click does not have any hard requirements for the return values and does not use them 
  itself. This allows return values to be used for custom decorators or workflows (like in
  the multi command chaining example).

--When a Click script is invoked as command line application (through BaseCommand.main())
  the return value is ignored unless the standalone_mode is disabled in which case it%u2019s 
  bubbled through.

Comments