Pycli is a python package that facilitates building cli-tools.
The Pycli library is built around pydantic allowing for flexible argument validation and transformation, Pycli also offers a pretty default --help flag on both the CLI and Command level as well as pretty formatting for validation errors built using rich.
Install with pip or any other python package manager
python -m pip install pycliThe basic usage envolves instantiating a Cli object and use the available method register_command to add it to the cli and calling .run method in a __name__ == __main__ block.
from pycli import Cli
cli = Cli(
name="file-tools",
version="1.0.0",
description="A CLI tool that contains various file utilities all in one place"
)
@cli.register_command(
name="stats",
description="Displays statistics about a given file if exists."
)
def stats(filename : str):
print(f"File {filename} : size : 10.59Kb.")
if __name__ == '__main__':
cli.run()A help flag will be available at the CLI level as well as at the individual command level :
A version flag will also be available at the CLI's level :
Pycli also comes with nice formatted error for when the user attempts to execute an unknown command or use an undefined flag :
Pycli generates a pydantic model from the function's signature used to validate the input arguments, any error are formatted and displayed :
You can add usage examples to be displayed when the help flag is used on the command level or the CLI level. On the CLI level the first example from each command is displayed.
from pycli import Cli, Example
cli = ... # Create CLI instance
@cli.register_command(
name="stats",
description="Displays statistics about a given file if exists.",
examples=[
Example(
args = { "filename" : "data.csv" },
description="Display statistics for the file 'data.csv'."
)
]
)
def stats(filename : str):
...
# Rest of the codeThe output for the CLI and command level :
The register_command decorator can also be used for classes, all the arguments have to be passed through the contructor and the __call__ method must be defined with no parameters :
from pycli import Cli, Example
cli = Cli(...)
@cli.register_command(
name="stats",
description="Displays statistics about a given file if exists.",
)
class StatsCommand:
def __init__(self, filename : str):
self.filename = filename
def __call__(self):
print(f"File {self.filename} : size : 10.59Kb.")
if __name__ == '__main__':
cli.run()Pycli supports highly structured inputs by defining an argument as being another pydantic class :
cli = Cli(...)
class ModelConfig(BaseModel):
act : str
num_layers : int
@cli.register_command(
name="stats",
description="Displays statistics about a given file if exists.",
examples=[
Example(
args = {
"epochs" : 5,
"lr" : 0.001,
"model_conf" : {
"act" : "relu",
"num_layers" : 3
}
},
description="Display statistics for the file 'data.csv'."
)
]
)
def train(epochs : int, lr : float, model_conf : ModelConfig):
...
if __name__ == '__main__':
cli.run()For a CLI that supports a large number of commands it is convenient to define them in several files and have them registered automatically :
project
├── commands
│ └── stats_cmd.py
├── instance.py
├── main.py
- instance.py :
from pycli import Cli
cli = Cli(
name="ml-tools",
description="A CLI tool that contains various Machine Learning models all in one place",
version="1.0.0"
)- main.py :
from instance import cli
if __name__ == '__main__':
cli.scan_dir("commands", pattern_or_predicate="(.+)_cmd.py")
cli.run()- commands/stats.py :
from instance import cli
@cli.register_command(
name="stats",
description="Displays statistics about a given file if exists."
)
def stats(filename : str):
print(f"File {filename} : size : 10.59Kb.")Another way to structure your commands is by using groups :
- main.py :
from pycli import Cli
from commands.file_tools import group as ft_grp
cli = Cli(
name="ml-tools",
description="A CLI tool that contains various Machine Learning models all in one place",
version="1.0.0"
)
if __name__ == '__main__':
cli.add_group(ft_grp)
cli.run()- commands/file_tools.py :
from pycli import Group
group = Group()
@group.register_command(
name="stats",
description="Displays statistics about a given file if exists."
)
def stats(filename : str):
print(f"File {filename} : size : 10.59Kb.")







