How require options for CLI app based on Python and Click - python

I am building a CLI app with Python and the Click library.
How do I achieve the following use case:
First I only want the subcommand to be followed by an argument no options are required:
$ myapp subcommand argument
This is straight forward.
But how can I write the code that if argument2 is set that also some options are required?
$ myapp subcommand argument2 -o1 abc -o2 def
For example:
no options are required:
$ ./myapp.py install basic
options are required:
$ ./myapp.py install custom -o1 abc -o2 def
Furthermore I do not know how to make choice for arguments, that means that the user must choose between "basic" or "custom". In case he chooses "custom", he needs to add some options.

I have achieved this successfully by making your argument2 be a click.Command.
Running through the code below, my main way of interacting with the CLI application is via the cli group. That cli group has another group, install, added as a command. So we have a CLI with nested groups.
install has 2 commands, basic and custom, as in your example.
basic takes no parameters, while custom takes 2 required Options.
Calls would look like this:
❯ myapp install custom -o1 arg1 -o2 def
This is a custom install with option1: arg1 and option2: def
❯ myapp install basic
Executing a basic install
You can see the nested group install acts as a command inside the help message:
❯ myapp
Usage: myapp [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
install
And if you were to invoke install, this is the help output you'd get.
❯ myapp install
Usage: myapp install [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
basic
custom
This is the code:
import click
#click.group()
def cli():
pass
#click.group()
def install():
pass
#install.command()
def basic():
print('Executing a basic install')
#install.command()
#click.option("-o1", "--option1", required=True)
#click.option("-o2", "--option2", required=True)
def custom(option1, option2):
print(f'This is a custom install with option1: {option1} and option2: {option2}')
def main():
cli.add_command(install)
cli()
if __name__ == '__main__':
main()

Related

How can I get a --version to the root of a typer.Typer application?

My CLI applications typically have subcommands. I want to have the --version flag at the root of my CLI applications, but with Typer I've only seen ways to put it to a command. I want to add it to the typer.Typer object (the root) itself. How can I do that?
What I've tried
import typer
from typing import Optional
__version__ = "0.1.0"
def version_callback(value: bool):
if value:
typer.echo(f"Awesome CLI Version: {__version__}")
raise typer.Exit()
app = typer.Typer(
add_completion=False,
)
#app.command()
def main(
version: Optional[bool] = typer.Option(
None, "--version", callback=version_callback
),
) -> None:
pass
#app.command()
def foo() -> None:
pass
if __name__ == "__main__":
app()
This gives
$ python cli.py --help
Usage: cli.py [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
foo
main
$ python cli.py main --help
Usage: cli.py main [OPTIONS]
Options:
--version
--help Show this message and exit.
What I wanted:
$ python cli.py --help
Usage: cli.py [OPTIONS] COMMAND [ARGS]...
Options:
--version
--help Show this message and exit.
Commands:
foo
This is addressed in the documentation:
But as those CLI parameters are handled by each of those commands, they don't allow us to create CLI parameters for the main CLI application itself.
But we can use #app.callback() for that.
It's very similar to #app.command(), but it declares the CLI parameters for the main CLI application (before the commands):
To do what you want, you could write something like this:
import typer
from typing import Optional
__version__ = "0.1.0"
def version_callback(value: bool):
if value:
typer.echo(f"Awesome CLI Version: {__version__}")
raise typer.Exit()
app = typer.Typer(
add_completion=False,
)
#app.callback()
def common(
ctx: typer.Context,
version: bool = typer.Option(None, "--version", callback=version_callback),
):
pass
#app.command()
def main() -> None:
pass
#app.command()
def foo() -> None:
pass
if __name__ == "__main__":
app()
Which gives us:
$ python typertest.py --help
Usage: typertest.py [OPTIONS] COMMAND [ARGS]...
Options:
--version
--help Show this message and exit.
Commands:
foo
main

Pass commandline arguments to a Python script installed with Poetry

The poetry documentation says that the script section can be used to install scripts or executable when the package is installed. But it does not show any example of how to pass arguments to the script.
How can you do to receive with argparse the arguments in the function?
First a little project setup:
Starting from a new poetry project with poetry new example_script (and creating a main.py file inside example_script dir) with a structure like this:
├── example_script
│   ├── __init__.py
│   ├── main.py
├── pyproject.toml
├── README.rst
└── tests
├── __init__.py
└── test_poetry_example.py
And adding in the pyproject.toml the config (in the section [tool.poetry.scripts]) of the script that we are going to install:
# pyproject.toml
[tool.poetry]
name = "example_script"
# some lines excluded
[tool.poetry.scripts]
my-script = "example_script.main:start"
# some lines excluded
And finally the main.py file, which has to have a start function inside (as we passed it in the toml). The arguments parser goes inside this function, since this function is the one that will end up executing when we run the script:
import argparse
def some_function(target, end="!"):
"""Some example funcion"""
msg = "hi " + target + end
print(msg)
def start():
# All the logic of argparse goes in this function
parser = argparse.ArgumentParser(description='Say hi.')
parser.add_argument('target', type=str, help='the name of the target')
parser.add_argument('--end', dest='end', default="!",
help='sum the integers (default: find the max)')
args = parser.parse_args()
some_function(args.target, end=args.end)
We can run the script with poetry, or install and run it directly:
# run with poetry
$ poetry run my-script
# install the proyect (this will create a virtualenv if you didn't have it created)
$ poetry install
# activate the virtualenv
$ poetry shell
# run the script
$ my-script --help
usage: my-script [-h] [--end END] target
Say hi.
positional arguments:
target the name of the target
optional arguments:
-h, --help show this help message and exit
--end END sum the integers (default: find the max)
$ my-script "spanish inquisition" --end "?"
hi spanish inquisition?
This question is really two separate questions:
How do I pass arguments into a script that is run using Poetry
How do I access and parse those arguments, in particular, using argparse
The initial answer (by Lucas), addresses parts of each, especially about argparse, but I'm answering to fill in some additional details and explain how to directly access the args.
Access arguments directly in any function or script
As an alternative to argparse, arguments can be directly accessed in Python at any time using sys.argv, which is a list of strings, each one is one of the arguments. Python splits up the arguments based on spaces, unless the spaces are enclosed in quotes (either single or double quotes).
This method is more direct and lightweight than argparse, with a lot less functionality.
args.py setup as a main script file with a start() function:
import sys
def start(args=sys.argv):
for i, arg in enumerate(args):
print(f'Arg #{i}: {arg}')
if __name__ == '__main__':
start()
Run it at the command-line with a variety of argument types:
$ python args.py "item 1" 'Hello Arguments!!' "i 3" 4 5 6
Arg #0: args.py
Arg #1: item 1
Arg #2: Hello Arguments!!
Arg #3: i 3
Arg #4: 4
Arg #5: 5
Arg #6: 6
The first argument is always the script that was called, in exactly the way it was called (i.e. relative or absolute path to the script file or other reference).
Adding arguments when calling with poetry run
While you can run scripts with Poetry by activating the virtual environment with poetry shell and then running the script as normal with python script.py arg1 arg2 arg3, you can also add arguments directly to the poetry run command:
At the command-line, directly running the script:
$ poetry run python args.py arg1 arg2 arg3
Arg #0: <some_path>/args.py
Arg #1: arg1
Arg #2: arg2
Arg #3: arg3
Running a python file as an installed Poetry script
Or, run it as a script, installed by Poetry. In this case the script name we assign is arg_script, and you just run it directly at a terminal prompt with the virtual environment activated (i.e. do not invoke with python):
In pyproject.toml:
[tool.poetry.scripts]
arg_script = 'args:start' # run start() function from ./args.py
After updating pyproject.toml, run poetry install at a terminal prompt to install the script in the virtual environment named as arg_script.
With Poetry, you can run a command in the virtual environment by using poetry run:
$ poetry run arg_script arg1 arg2 arg3
Arg #0: arg_script
Arg #1: arg1
Arg #2: arg2
Arg #3: arg3
Any arguments added after poetry run, act just like you were typing them into a terminal that has the virtual environment already activated. i.e. The equivalent is:
$ poetry shell
$ args_script arg1 arg2 arg3

git subcommand with --help arg, not working

Having this example code:
#!/usr/bin/env python3
import argparse
def main():
parser = argparse.ArgumentParser(description="Some Description")
parser.add_argument('some-arg')
args = parser.parse_args()
print(args)
if __name__ == '__main__':
main()
I add this code to file called git-mycommand, made it executable and copied it to /usr/bin.
Now trying to run command with --help, gives me this unintended output:
user#user:~$ git mycommand --help
No manual entry for git-mycommand
See 'man 7 undocumented' for help when manual pages are not available.
If I run command normally without --help, It works properly, like:
oerp#oerp:~$ git mycommand some_val
Namespace(**{'some-arg': 'some_val'})
Or if I dont use it as git subcommand and run it directly, like:
oerp#oerp:~$ git-mycommand --help
usage: git-mycommand [-h] some-arg
Some Description
positional arguments:
some-arg
optional arguments:
-h, --help show this help message and exit
Does anyone know why custom git subcommand does not work properly with --help argument? Or maybe there is something else, I need to do, so it would show intended output?
The git command is receiving the --help option, not your subcommand.
Note that git --help ... is identical to git help ... because the former is internally converted into the latter.
https://git-scm.com/docs/git-help
git help invokes git-help which opens the man page for the given command.

slow response of CLI written with Python Click

I'm experiencing slow responses of my CLI written with Click 7.0 on Python 3.6.6 (under conda environment).
It takes time to print the help message when calling the CLI when the package has been installed with pip (using setuptools):
$ time cli
Usage: cli [OPTIONS] COMMAND [ARGS]...
Welcome in the CLI!
Options:
--version Show the version and exit.
--help Show this message and exit.
real 0m0,523s
user 0m0,482s
sys 0m0,042s
However, I don't get this lag when calling the CLI directly from the source:
$ time python myproject/cli.py
Usage: cli.py [OPTIONS] COMMAND [ARGS]...
Welcome in the CLI!
Options:
--version Show the version and exit.
--help Show this message and exit.
real 0m0,088s
user 0m0,071s
sys 0m0,016s
Here is the content of myproject/cli.py:
import click
#click.group('cli', invoke_without_command=True)
#click.pass_context
#click.version_option(version='0.0.1', prog_name="test")
def cli(ctx):
"""
Welcome in the CLI!
"""
if ctx.invoked_subcommand is None:
# show help if no option passed to cli
if all(v==False for v in ctx.params.values()):
click.echo(ctx.get_help())
if __name__ == '__main__':
cli()
And setup.py is configured like this:
setup(
name=name,
version=__version__,
packages=find_packages(),
install_requires=install_requires,
author=author,
author_email=author_email,
description=description,
entry_points='''
[console_scripts]
cli=myproject.cli:cli
''',
keywords=keywords,
cmdclass=cmdclass,
include_package_data=True,
)
Could someone help me with this? This is really inconvenient to get such lag for a CLI.
For small Python CLIs this delay is very noticable. It has to do with the wrapper that setuptools creates around your CLI endpoint.
It implements some auxiliary functionality with your endpoint, like checking that your (virtual) python environment has all required dependencies.
People have created solutions to circumvent these auxilary functionalities with tools like fast-entry_points. Check it out, it might suite your use-case.
Note: This speed improvement is mostly noticeable for small CLIs. If you have a larger CLI/Project, you will need to structure your imports as local imports to prevent all imports being loaded when you perform a specific action. Especially when using auto-complete on your CLI, it might be worth to also change your imports.

ipython not showing argparse help message

In my python script myscript.py I use argparse to pass command-line arguments. When I want to display the help information about the input arguments, I just do:
$ python myscript.py --help
If instead I want to use ipython to run my script, the help message won't be displayed. Ipython will display its own help information:
$ ipython -- myscript.py -h
=========
IPython
=========
Tools for Interactive Computing in Python
=========================================
A Python shell with automatic history (input and output), dynamic object
introspection, easier configuration, command completion, access to the
system shell and more. IPython can also be embedded in running programs.
Usage
ipython [subcommand] [options] [files]
It's not so annoying, but is there a way around it?
You need to run your .py script inside the ipython. Something like that:
%run script.py -h
This is an IPython bug, corrected in https://github.com/ipython/ipython/pull/2663.
My 0.13 has this error; it is corrected in 0.13.2. The fix is in IPthyon/config/application.py Application.parse_command_line. This function looks for help and version flags (-h,-V) in sys.argv before passing things on to parse_known_args (hence the custom help formatting). In the corrected release, it checks sys.argv only up to the first --. Before it looked in the whole array.
earlier:
A fix for earlier releases is to define an alternate help flag in the script:
simple.py script:
import argparse, sys
print(sys.argv)
p = argparse.ArgumentParser(add_help=False) # turn off the regular -h
p.add_argument('-t')
p.add_argument('-a','--ayuda',action=argparse._HelpAction,help='alternate help')
print(p.parse_args())
Invoke with:
$ ./ipython3 -- simple.py -a
['/home/paul/mypy/argdev/simple.py', '-a']
usage: simple.py [-t T] [-a]
optional arguments:
-t T
-a, --ayuda alternate help
$ ./ipython3 -- simple.py -t test
['/home/paul/mypy/argdev/simple.py', '-t', 'test']
Namespace(t='test')

Categories

Resources