Advanced Arguments
Argument inheritance¶
Say you have arguments that you want every command to be able to use but you want to avoid having to copy paste their definition over and over on every command. Clypi provides an intuitive solution for this issue: argument inheritance.
The idea is easy: define the arguments in a parent command and all children will be able to use them without having to redefine them.
from clypi import Command, Positional, arg
class Wave(Command):
"""Wave at someone"""
name: Positional[str]
verbose: bool = arg(inherited=True)
async def run(self) -> None:
print(f"👋 Hey {self.name}")
if self.verbose:
print(f"👋👋👋 HEYYY {self.name}")
class Cli(Command):
"""A very simple CLI"""
verbose: bool = arg(
False, short="v", help="Whether to show verbose output", group="global"
)
subcommand: Wave | None
if __name__ == "__main__":
cmd = Cli.parse()
cmd.start()
You will see even though the help message for verbose
is defined in the parent command,
the subcommand Wave
gets the entire argument definition for free:
Usage: cli wave [NAME] [OPTIONS]
┏━ Arguments ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ [NAME] ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┏━ Global options ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ -v, --verbose Whether to show verbose output ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
python cli.py Daniel👋 Hey Daniel
python cli.py Daniel -v👋 Hey Daniel
👋👋👋 HEYYY Daniel
Deferring arguments¶
CLIs can get very complex. Sometimes we want to build a complex graph of dependencies between the arguments and it is hard to do that. For example, we can have an application that does not use --num-cores
if --single-threaded
was provided already. For that, clypi offers arg(defer=True)
.
The internals are complex but the user experience is quite simple: clypi will not prompt or require this value being passed up until when it's executed.
from clypi import Command, arg
class Cli(Command):
single_threaded: bool = arg(False)
num_cores: int = arg(
defer=True,
prompt="How many CPU cores do you want to use?"
)
async def run(self):
print(f"Running single theaded:", self.single_threaded) # << will not prompt yet...
if self.single_threaded:
# if we never access num_cores in this if condition, we will
# never prompt!
print("Running single threaded...")
else:
threads = self.num_cores // 4 # << we prompt here!
print("Running with threads:", threads)
if __name__ == "__main__":
cmd = Cli.parse() # << will not prompt yet...
cmd.start() # << will not prompt yet...
As you can see, we are prompted only if we do not specify --single-threaded
and only
after we've printed the "Running single threaded: False"
message:
Running single threaded...
python cli.pyRunning single theaded: False
How many CPU cores do you want to use?: 16
Running with threads: 4
Custom parsers¶
If the type you want to parse from the user is too complex, you can define your own parser
using config
as well: