47

I have a function which is wrapped as a command using click. So it looks like this:

@click.command()
@click.option('-w', '--width', type=int, help="Some helping message", default=0)
[... some other options ...]
def app(width, [... some other option arguments...]):
    [... function code...]

I have different use cases for this function. Sometimes, calling it through the command line is fine, but sometime I would also like to call directly the function

from file_name import app
width = 45
app(45, [... other arguments ...]) 

How can we do that? How can we call a function that has been wrapped as a command using click? I found this related post, but it is not clear to me how to adapt it to my case (i.e., build a Context class from scratch and use it outside of a click command function).

EDIT: I should have mentioned: I cannot (easily) modify the package that contains the function to call. So the solution I am looking for is how to deal with it from the caller side.

1
  • It's not clear enough (for me) what you have given externally. Commented Feb 5, 2018 at 10:09

6 Answers 6

25

I tried with Python 3.7 and Click 7 the following code:

import click

@click.command()
@click.option('-w', '--width', type=int, default=0)
@click.option('--option2')
@click.argument('argument')
def app(width, option2, argument):
    click.echo("params: {} {} {}".format(width, option2, argument))
    assert width == 3
    assert option2 == '4'
    assert argument == 'arg'


app(["arg", "--option2", "4", "-w", 3], standalone_mode=False)

app(["arg", "-w", 3, "--option2", "4" ], standalone_mode=False)

app(["-w", 3, "--option2", "4", "arg"], standalone_mode=False)

All the app calls are working fine!

Sign up to request clarification or add additional context in comments.

4 Comments

I get only one call: params: 3 4 arg in py 3.6.8, click 7.0. Inconvenient, that one cannot call app(1,2,3) anymore...
@jaromrax Do you mean that after the first app call the program exits? I had this issue and fixed it by surrounding each app call with a try except statement. In particular except SystemExit
Nice. This was the simplest solution for me. T
Run with standalone_mode=False to run multiple commands: example: app(["arg", "--option2", "4", "-w", 3], standalone_mode=False)
15

This use-case is described in the docs.

Sometimes, it might be interesting to invoke one command from another command. This is a pattern that is generally discouraged with Click, but possible nonetheless. For this, you can use the Context.invoke() or Context.forward() methods.

cli = click.Group()

@cli.command()
@click.option('--count', default=1)
def test(count):
    click.echo('Count: %d' % count)

@cli.command()
@click.option('--count', default=1)
@click.pass_context
def dist(ctx, count):
    ctx.forward(test)
    ctx.invoke(test, count=42)

They work similarly, but the difference is that Context.invoke() merely invokes another command with the arguments you provide as a caller, whereas Context.forward() fills in the arguments from the current command.

2 Comments

There is a valid use case for this: setting up automatic scripts using setuptools.py, which will require some wrapped invocation (since they do not accept args).
Your example only shows how to invoke a command from another click command, which is a rarely what you need to do when invoking a command from a program. The question specifically says "Sometimes, calling it through the command line is fine, but sometime I would also like to call directly the function" which hints that a solution invoked from the command line will not answer the question.
11

You can call a click command function from regular code by reconstructing the command line from parameters. Using your example it could look somthing like this:

call_click_command(app, width, [... other arguments ...])

Code:

def call_click_command(cmd, *args, **kwargs):
    """ Wrapper to call a click command

    :param cmd: click cli command function to call 
    :param args: arguments to pass to the function 
    :param kwargs: keywrod arguments to pass to the function 
    :return: None 
    """

    # Get positional arguments from args
    arg_values = {c.name: a for a, c in zip(args, cmd.params)}
    args_needed = {c.name: c for c in cmd.params
                   if c.name not in arg_values}

    # build and check opts list from kwargs
    opts = {a.name: a for a in cmd.params if isinstance(a, click.Option)}
    for name in kwargs:
        if name in opts:
            arg_values[name] = kwargs[name]
        else:
            if name in args_needed:
                arg_values[name] = kwargs[name]
                del args_needed[name]
            else:
                raise click.BadParameter(
                    "Unknown keyword argument '{}'".format(name))


    # check positional arguments list
    for arg in (a for a in cmd.params if isinstance(a, click.Argument)):
        if arg.name not in arg_values:
            raise click.BadParameter("Missing required positional"
                                     "parameter '{}'".format(arg.name))

    # build parameter lists
    opts_list = sum(
        [[o.opts[0], str(arg_values[n])] for n, o in opts.items()], [])
    args_list = [str(v) for n, v in arg_values.items() if n not in opts]

    # call the command
    cmd(opts_list + args_list)

How does this work?

This works because click is a well designed OO framework. The @click.Command object can be introspected to determine what parameters it is expecting. Then a command line can be constructed that will look like the command line that click is expecting.

Test Code:

import click

@click.command()
@click.option('-w', '--width', type=int, default=0)
@click.option('--option2')
@click.argument('argument')
def app(width, option2, argument):
    click.echo("params: {} {} {}".format(width, option2, argument))
    assert width == 3
    assert option2 == '4'
    assert argument == 'arg'


width = 3
option2 = 4
argument = 'arg'

if __name__ == "__main__":
    commands = (
        (width, option2, argument, {}),
        (width, option2, dict(argument=argument)),
        (width, dict(option2=option2, argument=argument)),
        (dict(width=width, option2=option2, argument=argument),),
    )

    import sys, time

    time.sleep(1)
    print('Click Version: {}'.format(click.__version__))
    print('Python Version: {}'.format(sys.version))
    for cmd in commands:
        try:
            time.sleep(0.1)
            print('-----------')
            print('> {}'.format(cmd))
            time.sleep(0.1)
            call_click_command(app, *cmd[:-1], **cmd[-1])

        except BaseException as exc:
            if str(exc) != '0' and \
                    not isinstance(exc, (click.ClickException, SystemExit)):
                raise

Test Results:

Click Version: 6.7
Python Version: 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)]
-----------
> (3, 4, 'arg', {})
params: 3 4 arg
-----------
> (3, 4, {'argument': 'arg'})
params: 3 4 arg
-----------
> (3, {'option2': 4, 'argument': 'arg'})
params: 3 4 arg
-----------
> ({'width': 3, 'option2': 4, 'argument': 'arg'},)
params: 3 4 arg

Comments

11

If you just want to call the underlying function, you can directly access it as click.Command.callback. Click stores the underlying wrapped Python function as a class member. Note that directly calling the function will bypass all Click validation and any Click context information won't be there.

Here is an example code that iterates all click.Command objects in the current Python module and makes a dictionary of callable functions out from them.

from functools import partial
from inspect import getmembers

import click


all_functions_of_click_commands = {}

def _call_click_command(cmd: click.Command, *args, **kwargs):
    result = cmd.callback(*args, **kwargs)
    return result

# Pull out all Click commands from the current module
module = sys.modules[__name__]
for name, obj in getmembers(module):
    if isinstance(obj, click.Command) and not isinstance(obj, click.Group):
        # Create a wrapper Python function that calls click Command.
        # Click uses dash in command names and dash is not valid Python syntax
        name = name.replace("-", "_") 
        # We also set docstring of this function correctly.
        func = partial(_call_click_command, obj)
        func.__doc__ = obj.__doc__
        all_functions_of_click_commands[name] = func

A full example can be found in binance-api-test-tool source code.

Comments

5

Building on Pierre Monico's answer: if you don't want to call the command from another command, you can also create the Context object yourself:

@click.command()
@click.option('--count', default=1)
def test(count):
    click.echo('Count: %d' % count)


ctx = click.Context(test)
ctx.forward(test, count=42)

Using invoke() is possible too, of course. As mentioned before, the difference is that forward() also fills in defaults.

Comments

0

To call a command programmatically (only if you are not using nested subcommands or "groups" with contexts, more on that to follow) you can use command.callback(). Here is a basic example:

# thing.py
import click

@click.command()
def cmd1():
  print(":)")

# thing2.py
from thing import cmd1

cmd1.callback()
# :)

This also works fine in the case that cmd1 is a subcommand of a group, e.g.

@click.group()
def my_group():
    pass


@my_group.command()
@click.pass_context
def cmd1():
    print(":)")

However, you'll find that it falls apart when using contexts:

# thing1.py
import click


@click.group()
@click.pass_context
def my_group(ctx):
    ctx.ensure_object(dict)
    ctx.obj["OK"] = True


@my_group.command()
@click.pass_context
def cmd1(ctx):
    assert "OK" in ctx.obj
    print(":)")


# thing2.py
from thing import cmd1

cmd1.callback() # RuntimeError: There is no active click context.
cmd1.callback(click.Context()) # RuntimeError: There is no active click context.
cmd1.callback(click.Context(my_group)) # RuntimeError: There is no active click context.

# Fails the assert, indicating the group portion which modifies the context
# is never called.
ctx = click.Context(cmd1)
ctx.forward(cmd1)

I have tried many ways which do not work. This is the only way I have found that works as I expect:

with my_group.make_context(
    # any descriptor you like
    "my_group",
    # arguments passed to my_group to invoke subcommand(s)
    ["cmd1"],
) as ctx:
    my_group.invoke(ctx)  # <- group and cmd1 runs

I would love to hear from someone that has found a better way, specifically one that allows the use of python objects (as callback does) instead of string arguments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.