Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Call another click command from a click command

I want to use some useful functions as commands. For that I am testing the click library. I defined my three original functions then decorated as click.command:

import click import os, sys  @click.command() @click.argument('content', required=False) @click.option('--to_stdout', default=True) def add_name(content, to_stdout=False):     if not content:         content = ''.join(sys.stdin.readlines())     result = content + "\n\tadded name"     if to_stdout is True:         sys.stdout.writelines(result)     return result   @click.command() @click.argument('content', required=False) @click.option('--to_stdout', default=True) def add_surname(content, to_stdout=False):     if not content:         content = ''.join(sys.stdin.readlines())     result = content + "\n\tadded surname"     if to_stdout is True:         sys.stdout.writelines(result)     return result  @click.command() @click.argument('content', required=False) @click.option('--to_stdout', default=False) def add_name_and_surname(content, to_stdout=False):     result = add_surname(add_name(content))     if to_stdout is True:         sys.stdout.writelines(result)     return result 

This way I am able to generate the three commands add_name, add_surname and add_name_and_surname using a setup.py file and pip install --editable . Then I am able to pipe:

$ echo "original content" | add_name | add_surname  original content      added name     added surname 

However there is one slight problem I need to solve, when composing with different click commands as functions:

$echo "original content" | add_name_and_surname  Usage: add_name_and_surname [OPTIONS] [CONTENT]  Error: Got unexpected extra arguments (r i g i n a l   c o n t e n t  ) 

I have no clue why it does not work, I need this add_name_and_surname command to call add_name and add_surname not as command but as functions, else it defeats my original purpose of using functions as both library functions and commands.

like image 517
kaligne Avatar asked Oct 17 '16 16:10

kaligne


People also ask

How do you call a click function in Python?

You can also call a Click function using the callback member function under some conditions. As per this GitHub issue: Assuming you know the given command is a direct wrapper for a function you wrote (and not a group or other type of command), you can get at the function with command. callback.

What is click command?

Click is a Python package for creating beautiful command line interfaces in a composable way with as little code as necessary. It's the “Command Line Interface Creation Kit”. It's highly configurable but comes with sensible defaults out of the box.

What is Python3 command?

The Python3 command was introduced because the python command pointed to python2. Since then, Python3 has become the default and thus python points to python3 on most but not all systems. So, most developers explicitly use python2 and python3 as to not run into issues on other systems.


2 Answers

When you call add_name() and add_surname() directly from another function, you actually call the decorated versions of them so the arguments expected may not be as you defined them (see the answers to How to strip decorators from a function in python for some details on why).

I would suggest modifying your implementation so that you keep the original functions undecorated and create thin click-specific wrappers for them, for example:

def add_name(content, to_stdout=False):     if not content:         content = ''.join(sys.stdin.readlines())     result = content + "\n\tadded name"     if to_stdout is True:         sys.stdout.writelines(result)     return result  @click.command() @click.argument('content', required=False) @click.option('--to_stdout', default=True) def add_name_command(content, to_stdout=False):     return add_name(content, to_stdout) 

You can then either call these functions directly or invoke them via a CLI wrapper script created by setup.py.

This might seem redundant but in fact is probably the right way to do it: one function represents your business logic, the other (the click command) is a "controller" exposing this logic via command line (there could be, for the sake of example, also a function exposing the same logic via a Web service for example).

In fact, I would even advise to put them in separate Python modules - Your "core" logic and a click-specific implementation which could be replaced for any other interface if needed.

like image 152
shevron Avatar answered Oct 17 '22 23:10

shevron


Due to the click decorators the functions can no longer be called just by specifying the arguments. The Context class is your friend here, specifically:

  1. Context.invoke() - invokes another command with the arguments you supply
  2. Context.forward() - fills in the arguments from the current command

So your code for add_name_and_surname should look like:

@click.command() @click.argument('content', required=False) @click.option('--to_stdout', default=False) @click.pass_context def add_name_and_surname(ctx, content, to_stdout=False):     result = ctx.invoke(add_surname, content=ctx.forward(add_name))     if to_stdout is True:         sys.stdout.writelines(result)     return result 

Reference: http://click.pocoo.org/6/advanced/#invoking-other-commands

like image 44
Vikas Tikoo Avatar answered Oct 17 '22 23:10

Vikas Tikoo