How do I return a value when @click.option is used to pass a command line argument to a function?
Asked Answered
F

7

18

I am trying to use click python package to pass a command line argument to a function. The example from official documentation works as explained. But nowhere in the documentation is it mentioned how to return a value back. None of the functions in the documentation returns a value, so I don't understand how to do this.

Given example at documentation:

import click

@click.command()
@click.option('--count', default=3, help='Number of greetings.')

def hello(count):
    """Simple program that greets NAME for a total of COUNT times."""
    for x in range(count):
        click.echo('Hello')

if __name__ == '__main__':
    hello()

What I want to do:

import click

@click.command()
@click.option('--count', default=3, help='Number of greetings.')

def hello(count):
    """Simple program that greets NAME for a total of COUNT times."""
    for x in range(count):
        click.echo('Hello')
    return "hello hi"

if __name__ == '__main__':
    print(hello())

"hello hi" is not returned as output on console. Can someone tell me how to achieve this?

Foremast answered 7/10, 2014 at 23:19 Comment(0)
B
13

Unfortunately, what you're trying to do doesn't make sense. Command-line programs can have an exit code, but that's just a small integer; they can't return text, or arbitrary Python objects.

There's a quasi-standard for what these integers means; the simple version is 0 for success, 1 for most errors, 2 for invalid command-line arguments. click is trying to make your function into a good command-line citizen, so when you exit your function, it calls sys.exit with the appropriate number (0 if you return, 1 if you raise, and 2 if it failed to parse your arguments).

So, whatever you return has no effect, and whatever you try to do with the return value at the top level doesn't even get run.

What programs usually do when they need to "return" text is to print it to standard output, which is exactly what click.echo is for.

Bessel answered 8/10, 2014 at 0:30 Comment(5)
why would I want to print to a console when I want a function to process something and return a value ? doesn't make sense to me. So if I use click over a function, can't I return a value to calling function ?Foremast
@simminni: When you use @click.command, what you have is no longer a function, it's a command-line program. There is nobody to return to. That's the whole point of @click.command. If you want a function to return a value to a caller, just don't make it a command.Bessel
@simminni: And the reason you want to print to a console is that you're building a command-line tool. That's how command-line tools interact with each other: by piping the output of one to the input of the next. If you don't want to do that, why are you building command-line tools in the first place?Bessel
I would argue that it makes sense if you're not piping output and just want to send configuration arguments to your command line tool (and parse them in the program as you go). E.g. I'm working with prototyping things in a machine learning project, and I'd really want the functionality for easy parsing of command line arguments.Macrospore
This has been incorrect since click version 3.0 (August 12th, 2014 which actually predates this answer) with the introduction of the standalone_mode flag. See my answer for details https://mcmap.net/q/680294/-how-do-i-return-a-value-when-click-option-is-used-to-pass-a-command-line-argument-to-a-functionVanir
B
5

By default, click converts your program into a command-line program, i.e. it executes the click command and exits with a return code. Thus your program executes hello and finishes:

import click

@click.command()
@click.option('--count', default=3, help='Number of greetings.')
def hello(count):
    """Simple program that greets NAME for a total of COUNT times."""
    for x in range(count):
        click.echo('Hello')
    return "hello hi"

if __name__ == '__main__':
    print(hello())

It prints:

Hello
Hello
Hello

You can change this behaviour by passing the argument standalone_mode=False (Click API Commands):

import click

@click.command()
@click.option('--count', default=3, help='Number of greetings.')    
def hello(count):
    """Simple program that greets NAME for a total of COUNT times."""
    for x in range(count):
        click.echo('Hello')
    return "hello hi"

if __name__ == '__main__':
    print(hello(standalone_mode=False))

It prints:

Hello
Hello
Hello
hello hi
Boehmenist answered 25/2, 2022 at 14:5 Comment(0)
V
2

After looking through click's source code/reference I stumbled across the main() method of BaseCommand which takes the parameter, standalone_mode, that allows you to return values if disabled (it is True by default). Hope this helps someone in the future.

Setup code:

import sys
import click

@click.group(invoke_without_command=True)
@click.option('--option1')
@click.argument('arg1')
def main(option1, arg1):
    class SpecialObject():
        def __init__(self, option, arg):
            self.option = option
            self.arg = arg
        def __repr__(self):
            return str(self.option)+str(self.arg)

    return SpecialObject(option1, arg1)

@main.command()
def subcmd():
    return [4,5,6]

Test code:

if __name__ == "__main__":
    commands = (
        ["--help"], 
        ["arg_val",],
        ["--option1","option1_val","arg1_val"],
        ["arg_val","subcmd"],
        ["arg_val","subcmd", "--help",], 
    )
    print(f'Click Version: {click.__version__}')
    print(f'Python Version: {sys.version}')
    for cmd in commands:
        print('-----------')
        print(f'Starting cmd:{cmd}')
        ret_val = main.main(cmd, standalone_mode=False)
        print(f"Returned: {type(ret_val)}, {ret_val}\n"

Output:

$ python __main__.py
Click Version: 7.1.2
Python Version: 3.9.1 (default, Dec 11 2020, 09:29:25) [MSC v.1916 64 bit (AMD64)]
-----------
Starting cmd:['--help']
Usage: __main__.py [OPTIONS] ARG1 COMMAND [ARGS]...

Options:
  --option1 TEXT
  --help          Show this message and exit.

Commands:
  subcmd
Returned: <class 'int'>, 0

-----------
Starting cmd:['arg_val']
Returned: <class '__main__.main.<locals>.SpecialObject'>, Nonearg_val

-----------
Starting cmd:['--option1', 'option1_val', 'arg1_val']
Returned: <class '__main__.main.<locals>.SpecialObject'>, option1_valarg1_val

-----------
Starting cmd:['arg_val', 'subcmd']
Returned: <class 'list'>, [4, 5, 6]

-----------
Starting cmd:['arg_val', 'subcmd', '--help']
Usage: __main__.py subcmd [OPTIONS]

Options:
  --help  Show this message and exit.
Returned: <class 'int'>, 0
Vanir answered 11/2, 2021 at 14:27 Comment(2)
Great answer - it took me a while to read the code and work out that this was exactly what I wanted though. In case it helps the next reader - If you usually invoke your cli as cli() and aren't seeing the returned value of your command as expected, use cli(standalone_mode=False) instead.Trainer
Glad it helped! I agree my answer could be made more brief; feel free to provide edits to the answer if you happen to create a shorter working example.Vanir
S
1

You have to tell click to print to stdout using click.echo(). Here is a working example using the code in your question (without caring about exit code as others have mentioned):

import click


@click.command()
@click.option('--count', default=3, help='Number of greetings.')
def hello(count):
    """Simple program that greets NAME for a total of COUNT times."""
    for x in range(count):
        click.echo('Hello')
    return click.echo("hello hi")

if __name__ == '__main__':
    hello()
Sanches answered 2/3, 2015 at 16:12 Comment(1)
See my answer if you'd like to return values that aren't only string values https://mcmap.net/q/680294/-how-do-i-return-a-value-when-click-option-is-used-to-pass-a-command-line-argument-to-a-functionVanir
T
1
import click

@click.command()
@click.option('--age', prompt=True, type=int, help='This will represent your age')
def main(age):
    """This is a age teller"""
    click.echo(f"Hello, your age is {age}")
    return 10 + age

def sub():
    age = main(standalone_mode=False)
    print(f"Return value from the main function is: {age}")

if __name__ == '__main__':
    sub()

Age: 5 Hello, your age is 5 Return value from the main function is: 15

Tuttifrutti answered 23/10, 2023 at 17:47 Comment(0)
A
0

See the click docs here also this answer here

# Main Runtime call at bottom of code
hello(standalone_mode=False)

# or e.g
main(standalone_mode=False)
Avilla answered 15/6, 2022 at 21:52 Comment(2)
You should upvote existing answers instead of duplicating them.Quotable
"The simplest explanation is usually the best one." - Occam's RazorAvilla
B
-3

Apparently this click package makes your hello() function never return. So the print never happens. If I were you, I would not use this unintuitive package and instead use argparse which is great at command line processing in Python (one of the best in any language, in my opinion).

Here's a working version of your code using argparse. It works out-of-the-box with Python (any version 2.7 or later), and it's easy to understand.

def hello(count):
    """Simple program that greets NAME for a total of COUNT times."""
    for x in range(count):
        print('Hello')
    return "hello hi"

if __name__ == '__main__':
    import argparse
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--count', default=3, type=int, help='Number of greetings.')
    args = parser.parse_args()

    print(hello(args.count))
Ballenger answered 8/10, 2014 at 0:30 Comment(3)
argparse rocks, but click adds a lot of functionality on top of it that in many cases would take more code than your actual logic. See Why Click? in the docs. It's perfectly reasonable to use it for its intended purpose.Bessel
@abarnert: I edited a working argparse version into my answer. It takes one extra line of code compared to the original with click.Ballenger
If you read the link I gave you, the point of click isn't for dead-simple programs, it's for things like composing complicated CLI programs. Saying "it only saves one line for this program" is like saying "C is just as good as Python because it's only 1 line longer for Hello, World".Bessel

© 2022 - 2024 — McMap. All rights reserved.