A new CLI¶
create cli generates an installable command-line application: a packaged
src/ layout project with a console entry point and a test.
1. Create and sync¶
$ pyfaro create cli greeter
Created CLI at ~/projects/greeter
$ cd greeter
$ pyfaro sync
Using CPython 3.13.11
Creating virtual environment at: .venv
Resolved 12 packages in 8ms
Building greeter @ file://~/projects/greeter
Built greeter @ file://~/projects/greeter
Prepared 1 package in 3ms
Installed 8 packages in 12ms
+ greeter==0.1.0 (from file://~/projects/greeter)
+ iniconfig==2.3.1
+ packaging==26.3
+ pluggy==1.7.0
+ pygments==2.21.0
+ pytest==9.1.1
+ ruff==0.16.10
+ ty==0.0.85
The command is declared in pyproject.toml:
2. Run the command¶
pyfaro run executes a program from the project
environment, so the console script is on its path:
Everything after -- belongs to the program, not to Pyfaro.
3. Make it do something¶
Replace src/greeter/cli.py with an argparse version:
"""Console entry point for greeter."""
import argparse
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="greeter", description="Greet someone.")
parser.add_argument("name", nargs="?", default="world", help="who to greet")
args = parser.parse_args(argv)
print(f"Hello, {args.name}!")
return 0
and update tests/test_cli.py:
"""Tests for the greeter command."""
import pytest
from greeter.cli import main
def test_default(capsys: pytest.CaptureFixture[str]) -> None:
assert main([]) == 0
assert capsys.readouterr().out == "Hello, world!\n"
def test_name(capsys: pytest.CaptureFixture[str]) -> None:
assert main(["Ada"]) == 0
assert capsys.readouterr().out == "Hello, Ada!\n"
$ pyfaro run -- greeter Ada
Hello, Ada!
$ pyfaro run -- greeter --help
usage: greeter [-h] [name]
Greet someone.
positional arguments:
name who to greet
options:
-h, --help show this help message and exit
$ pyfaro check
4 files already formatted
All checks passed!
All checks passed!
== pyfaro check summary ==
format: ok
lint: ok
type: ok
check: ok
$ pyfaro test
============================= test session starts ==============================
platform linux -- Python 3.13.11, pytest-9.1.1, pluggy-1.7.0
rootdir: ~/projects/greeter
configfile: pyproject.toml
testpaths: tests
collected 2 items
tests/test_cli.py .. [100%]
============================== 2 passed in 0.01s ===============================
4. Distribute¶
Build the distributions with pyfaro build, then
install the wheel as a tool:
$ pyfaro build
Building source distribution (uv build backend)...
Building wheel from source distribution (uv build backend)...
Successfully built dist/greeter-0.1.0.tar.gz
Successfully built dist/greeter-0.1.0-py3-none-any.whl
built dist/greeter-0.1.0.tar.gz
built dist/greeter-0.1.0-py3-none-any.whl
$ uv tool install ./dist/greeter-0.1.0-py3-none-any.whl
Resolved 1 package in 0.99ms
Prepared 1 package in 2ms
Installed 1 package in 1ms
+ greeter==0.1.0 (from file://~/projects/greeter/dist/greeter-0.1.0-py3-none-any.whl)
Installed 1 executable: greeter
$ greeter Ada
Hello, Ada!
greeter is now on your PATH, outside the project environment. Remove it
with uv tool uninstall greeter. To share it, upload dist/* with
uv publish, after which anyone can run uv tool install greeter.
Next steps¶
- The quality gate keeps the CLI tidy as it grows.
- Continuous integration runs checks on every push.
- See
createfor naming rules and generated files.