Note: This is a Jupyter notebook. You can explore this notebook interactively by clicking the "download" button in the top-right corner.
Yapx Examples¶
# Demo function
def demo(
name: str,
):
print(f"Hello {name}")
# Demo subcommand
def update_info(
age: int,
is_employed: bool,
net_worth: float = 0,
**kwargs,
):
print(f"age={age}; type={type(age).__name__}")
print(f"is_employed={is_employed}; type={type(is_employed).__name__}")
print(f"net_worth={net_worth}; type={type(net_worth).__name__}")
print(f"kwargs={kwargs}")
Create a parser and add arguments from the function demo
:
import yapx
parser = yapx.ArgumentParser()
parser.add_arguments(demo)
parser.print_help()
________________________________________________________________________________ $ awesome-app ________________________________________________________________________________
Helpful Parameters: --help, -h Show this help message. --help-all Show help for all commands. --print-shell-completion {bash,zsh,tcsh} Print shell completion script. --tui Show Textual User Interface (TUI). Required Parameters: --name <value>
Usage: awesome-app [--help | --help-all | --print-shell-completion {bash,zsh,tcsh} | --tui] --name <value>
Parse command-line arguments:
parsed_args = parser.parse_args(["--name", "Donald"])
parsed_args.to_dict()
{'name': 'Donald'}
Run the example demo
function with parsed args.
demo(name=parsed_args.name)
Hello Donald
Subcommands¶
Now, adding update_info
as a subcommand:
import yapx
parser = yapx.ArgumentParser()
parser.add_arguments(demo)
parser.add_command(update_info)
parser.print_help(include_commands=True)
________________________________________________________________________________ $ awesome-app ________________________________________________________________________________
Helpful Parameters: --help, -h Show this help message. --help-all Show help for all commands. --print-shell-completion {bash,zsh,tcsh} Print shell completion script. --tui Show Textual User Interface (TUI). Required Parameters: --name <value> Commands: <COMMAND> update-info
Usage: awesome-app [--help | --help-all | --print-shell-completion {bash,zsh,tcsh} | --tui] --name <value> <COMMAND> ... ________________________________________________________________________________ $ awesome-app update-info ________________________________________________________________________________
Helpful Parameters: --help, -h Show this help message. --tui Show Textual User Interface (TUI). Required Parameters: --age <#> --is-employed, --no-is-employed Optional Parameters: --net-worth <#> | Default: 0.0 <key=value ...> Any extra command-line key-value pairs.
Usage: awesome-app update-info [--help | --tui] --age <#> --is-employed [--net-worth <#>]
parsed_args = parser.parse_args(
["--name", "Jane", "update-info", "--age", "23", "--is-employed"]
)
parsed_args.to_dict()
{'name': 'Jane', 'age': 23, 'is_employed': True, 'net_worth': 0.0}
Yapx imposes this simple structure for CLI apps. The CLI parser can have one root command, and any number of subcommands and nested subcommands.
___________ _________________ _________ _____________
awesome-cli --log-level debug say-hello --name Donald
___________ _________________ _________ _____________
CLI App Root-args Command Command-args
yapx.build_parser¶
The methods add_arguments
and add_command
are encapsulated into one method build_parser
:
parser = yapx.build_parser(demo, [update_info])
parser.print_help(include_commands=True)
________________________________________________________________________________ $ awesome-app ________________________________________________________________________________
Helpful Parameters: --help, -h Show this help message. --help-all Show help for all commands. --print-shell-completion {bash,zsh,tcsh} Print shell completion script. --tui Show Textual User Interface (TUI). Required Parameters: --name <value> Commands: <COMMAND> update-info
Usage: awesome-app [--help | --help-all | --print-shell-completion {bash,zsh,tcsh} | --tui] --name <value> <COMMAND> ... ________________________________________________________________________________ $ awesome-app update-info ________________________________________________________________________________
Helpful Parameters: --help, -h Show this help message. --tui Show Textual User Interface (TUI). Required Parameters: --age <#> --is-employed, --no-is-employed Optional Parameters: --net-worth <#> | Default: 0.0 <key=value ...> Any extra command-line key-value pairs.
Usage: awesome-app update-info [--help | --tui] --age <#> --is-employed [--net-worth <#>]
parsed_args = parser.parse_args(
["--name", "Jane", "update-info", "--age", "23", "--is-employed"]
)
vars(parsed_args)
{'name': 'Jane', '_command_args_model_0': types.Dataclass_demo, '_command_parser': ArgumentParser(prog='awesome-app update-info', usage=None, description=None, formatter_class=<class 'rich_argparse.RawTextRichHelpFormatter'>, conflict_handler='error', add_help=False), '_command_func_0': <function __main__.demo(name: str)>, 'age': 23, 'is_employed': True, 'net_worth': 0.0, '_command_args_model_1': types.Dataclass_update_info, '_command_func_1': <function __main__.update_info(age: int, is_employed: bool, net_worth: float = 0, **kwargs)>}
In the parsed arguments, notice the keys that start with an underscore _
. These provide references to the root function, and any matching subcommand function.
parsed_args._command_func_0(name=parsed_args.name)
Hello Jane
parsed_args._command_func_1(**parsed_args.to_dict())
age=23; type=int is_employed=True; type=bool net_worth=0.0; type=float kwargs={'name': 'Jane'}
The parsed_args
namespace object features a method to_dict()
that will output the argument values as a dictionary, while excluding any keys that do not start with a letter:
parsed_args.to_dict()
{'name': 'Jane', 'age': 23, 'is_employed': True, 'net_worth': 0.0}
yapx.run¶
The process of adding arguments and commands is packed into yapx.build_parser
. The function yapx.run
extends this by parsing arguments, calling the root function, and the appropriate subcommand function, while sneaking in even more useful behavior:
yapx.run(
demo,
[update_info],
args=["--name", "Jane", "update-info", "--age", "23", "--is-employed"],
)
Hello Jane age=23; type=int is_employed=True; type=bool net_worth=0.0; type=float kwargs={}
Code within yapx.run
enables some magical behavior, like using the root function to facilitate setup/teardown logic with the yield
statement:
def setup():
print("Marco")
yield
print("Polo")
def exclaim():
print("!!!")
def question():
print("???")
def repeat(char):
print(char * 3)
yapx.run(setup, [exclaim, question, repeat], args=[])
Marco Polo
yapx.run(setup, [exclaim, question, repeat], args=["exclaim"])
Marco !!! Polo
yapx.run(setup, [exclaim, question, repeat], args=["question"])
Marco ??? Polo
yapx.run(setup, [exclaim, question, repeat], args=["repeat", "--char", "$"])
Marco $$$ Polo
yapx.run_commands¶
The root command is optional. To omit it, pass None
as the first positional arg to yapx.run
, or equivalently, call yapx.run_command
. E.g.:
yapx.run(None, [exclaim, question, repeat], args=[])
# OR
yapx.run_commands([exclaim, question, repeat], args=[])
yapx.cmd¶
Command names are inferred from the function name. To explicitly provide a command name, use the yapx.cmd
function to modify the command:
parser = yapx.build_parser(
None,
subcommands=[
yapx.cmd(exclaim, name="shout"),
yapx.cmd(question, name="doubt"),
yapx.cmd(repeat, name="echo"),
],
)
parser.print_help()
________________________________________________________________________________ $ awesome-app ________________________________________________________________________________
Helpful Parameters: --help, -h Show this help message. --help-all Show help for all commands. --print-shell-completion {bash,zsh,tcsh} Print shell completion script. --tui Show Textual User Interface (TUI). Commands: <COMMAND> shout doubt echo
Usage: awesome-app [--help | --help-all | --print-shell-completion {bash,zsh,tcsh} | --tui] <COMMAND> ...
yapx.Context¶
When a parameter is annotated with yapx.Context
, it is excluded from the command-line interface, and is populated with a Context()
object that gives access to some useful attributes.
def setup():
print("Marco")
yield "Niccolo"
print("Polo")
def exclaim(context: yapx.Context):
print(context.relay_value, "!!!")
def question(context: yapx.Context):
print(context.relay_value, "???")
def repeat(char, context: yapx.Context):
print(context.relay_value, char * 3)
yapx.run(setup, [exclaim, question, repeat], args=["question"])
Marco Niccolo ??? Polo
You can also use a mutable data structure, such as a dict
, to store shared state:
state = {}
def setup():
state["active"] = True
print("Marco", state)
yield
state["active"] = False
print("Polo", state)
def exclaim():
print("!!!", state)
def question():
print("???", state)
def repeat(char):
print(char * 3, state)
yapx.run(setup, [exclaim, question, repeat], args=["exclaim"])
Marco {'active': True} !!! {'active': True} Polo {'active': False}
Note that the root-command is always executed, and the appropriate sub-command is executed based on the given arguments.
However, yapx.run
accepts a parameter, defaults_args
, to use when no arguments are provided. This is useful, for example, to display help when no arguments are provided:
from contextlib import suppress
with suppress(SystemExit):
yapx.run(setup, [exclaim, question, repeat], args=[], default_args=["--help"])
________________________________________________________________________________ $ awesome-app ________________________________________________________________________________
Helpful Parameters: --help, -h Show this help message. --help-all Show help for all commands. --print-shell-completion {bash,zsh,tcsh} Print shell completion script. --tui Show Textual User Interface (TUI). Commands: <COMMAND> exclaim question repeat
Usage: awesome-app [--help | --help-all | --print-shell-completion {bash,zsh,tcsh} | --tui] <COMMAND> ...
Repeating Parameters¶
In Yapx, argument values can be provided multiple times. If the parameter type is a sequence (list
, set
, dict
, etc.), then repeated values are appended to the sequence. Otherwise, the parameter assumes only the latest value.
from typing import List, Optional
def demo(
name: str,
age: int,
is_employed: bool,
references: List[str],
net_worth: float = 0,
):
print(f"name={name}; type={type(name).__name__}")
print(f"age={age}; type={type(age).__name__}")
print(f"is_employed={is_employed}; type={type(is_employed).__name__}")
print(f"references={references}; type={type(references).__name__}")
print(f"net_worth={net_worth}; type={type(net_worth).__name__}")
yapx.run(
demo,
args=[
"--name",
"Jane",
"--name",
"John",
"--age",
"33",
"--age",
"66",
"--is-employed",
"--no-is-employed",
"--references",
"Joe",
"--references",
"Jerry",
],
)
name=John; type=str age=66; type=int is_employed=False; type=bool references=['Joe', 'Jerry']; type=list net_worth=0.0; type=float
Counting Parameters¶
A special case of repeating parameters is the "counting" parameter. The value passed to the argument is equal to the number of times the argument was provided.
To designate an argument as a counting parameter, it must be annotated with type int
and nargs=0
, e.g.:
from yapx.types import Annotated
def demo(verbose: Annotated[int, yapx.counting_arg("-v", "--verbose")]):
print("verbosity level:", verbose)
yapx.run(demo, args=["-vvvvv"])
verbosity level: 5
Notice the use of Annotated
coupled with yapx.arg
to modify the properties of an argument.
yapx.arg¶
Annotating an argument with yapx.arg(...)
exposes some essential argument properties, and enables some neat tricks like the "counting parameter" example above. Here's some more:
Positional Arguments¶
Arguments can be specified as positional like so:
def demo(repeat: Annotated[int, yapx.arg(pos=True)]):
print("Alright, " * repeat)
yapx.run(demo, args=["3"])
Alright, Alright, Alright,
Argument Help¶
def demo(chill: Annotated[bool, yapx.arg(help="Did I take my chill pill?")]):
"""This function provides a demo of argument help.
I didn't intend to also demonstrate function help from docstrings!
But here it is :)
"""
print("Chill?", chill)
parser = yapx.build_parser(demo)
parser.print_help()
________________________________________________________________________________ $ awesome-app ________________________________________________________________________________
This function provides a demo of argument help. I didn't intend to also demonstrate function help from docstrings! But here it is :) Helpful Parameters: --help, -h Show this help message. --help-all Show help for all commands. --print-shell-completion {bash,zsh,tcsh} Print shell completion script. --tui Show Textual User Interface (TUI). Required Parameters: --chill, --no-chill Did I take my chill pill?
Usage: awesome-app [--help | --help-all | --print-shell-completion {bash,zsh,tcsh} | --tui] --chill
Flags¶
Specify flags, including boolean-negation flags.
def demo(is_groovy: Annotated[bool, yapx.arg("--groovy/--not-groovy")]):
print("Groovy?", is_groovy)
yapx.run(demo, args=["--groovy", "--not-groovy", "--groovy"])
Groovy? True
Environment Variables¶
Default argument values can be populated from one or more environment variables:
import os
os.environ["EDITOR"] = "vim"
def demo(editor: Annotated[str, yapx.arg(env=["EDITOR"])]):
print("Editor:", editor)
yapx.run(demo, args=[])
Editor: vim
nargs¶
nargs specifies the number of arguments, or values, that a parameter accepts.
Note, the terminology is a bit muddy here; I would also consider it valid to say "nargs specifies the number of values that an argument accepts".
For example:
- When
nargs=0
:--value
- When
nargs=1
:--value a
- When
nargs=2
:--value a b
- When
nargs=3
:--value a b c
- When
nargs=*
:--value a b c ...
Feature-Flag Parameter¶
When specifying nargs=0
along with the type str
, the argument becomes a feature-flag. Because the parameter accepts no values, the resulting value is the name of the flag itself. For example:
def demo(target: Annotated[str, yapx.feature_arg("--dev", "--test", "--prod")]):
print("Deploying to:", target)
yapx.run(demo, args=["--prod"])
Deploying to: prod
Here is an parameter that accepts a list of values.
from yapx.types import Annotated
def demo(value: Annotated[List[str], yapx.unbounded_arg()]):
print(value)
yapx.run(demo, args=["--value", "tomatoe", "potatoe", "ramen-ato?"])
['tomatoe', 'potatoe', 'ramen-ato?']
Reusing Functions¶
You may find that you have a function that you want to call both (1) from the CLI, and (2) from within Python code. Further, suppose you want the function to behave differently dependening on what invoked it.
For example, when called from the CLI, I want results printed to the console, but when invoked from Python, I expect that the results are returned (not printed).
def demo(
x: Annotated[int, yapx.arg(default=1)] = 7,
y: Annotated[int, yapx.arg(default=2)] = 8,
z: Annotated[int, yapx.arg(default=3)] = 9,
context: Optional[yapx.Context] = None,
) -> List[int]:
print(
f"x={x} {type(x)}",
f"y={y} {type(y)}",
f"z={z} {type(z)}",
f"CLI context provided: {context is not None}",
sep="\n",
)
# Called from Yapx CLI
yapx.run(demo, args=[])
x=1 <class 'int'> y=2 <class 'int'> z=3 <class 'int'> CLI context provided: True
# Called directly from Python
demo()
x=7 <class 'int'> y=8 <class 'int'> z=9 <class 'int'> CLI context provided: False
Extra Arguments¶
By default, when an unrecognized argument is given, an error is raised.
However, if the function accepts *args
or **kwargs
, then any unrecognized arguments are accepted and passed along.
def demo(*args: str, **kwargs: int):
print("args:", args)
print("kwargs:", kwargs)
yapx.run(demo, args=["one=1", "two=2", "three=3"])
args: ('one=1', 'two=2', 'three=3') kwargs: {'one': 1, 'two': 2, 'three': 3}
Pydantic¶
yapx performs type-casting and validation of some basic, built-in Python types with no dependencies outside of the standary library. But if the pydantic
library is present, yapx will rely on it to support even more types. Install it using: pip install 'yapx[pydantic]'
>>> import yapx
... from typing import Pattern
...
>>> def is_match(text: str, pattern: Pattern) -> bool:
... return bool(pattern.fullmatch(text))
...
>>> yapx.run(is_match, _args=['--text', '123', '--pattern', '\\d+'])
# with pydantic:
True
# without pydantic:
UnsupportedTypeError: Unsupported type: typing.Pattern
"pip install 'yapx[pydantic]'" to support more types.
Constraining Input Values¶
Pydantic¶
Pydantic is awesome. It even comes with custom types that wrap around native types to enforce constraints on input values.
import yapx
from pydantic.types import confloat, constr
# `name` only accepts letters.
# `age_years` only accepts values between 0 and 200.
def demo(name: constr(pattern="^[A-Za-z]+$"), age_years: confloat(gt=0, lt=200)):
print(name, age_years)
yapx.run(demo, args=["--name", "Joe", "--age-years", "123"])
Joe 123.0
Feature-Flag Parameters¶
We've seen this before. A fine way of constraining input using command-line flags.
def demo(target: Annotated[str, yapx.feature_arg("--dev", "--test", "--prod")]):
print("Deploying to:", target)
yapx.run(demo, args=["--prod"])
Deploying to: prod
Literal¶
import yapx
from yapx.types import Literal
def demo(
value: Literal["one", "two", "three"],
):
print(value)
yapx.run(demo, args=["--value", "two"])
two
Enum¶
import yapx
from enum import Enum, auto
class TargetEnvironment(Enum):
dev = auto()
test = auto()
prod = auto()
def demo(
value: TargetEnvironment,
):
print(value)
yapx.run(demo, args=["--value", "prod"])
TargetEnvironment.prod
Note: Yapx does not support the annotation
List[Literal[...]]
, but sequences of enums are supported, e.g.,List[TargetEnvironment]
.
Another note: the use of
from __future__ import annotations
can cause issues with Yapx parsing type-hints when using non-native classes like customEnum
.