source: handbook ch. 53, 57
Python apps
Python is everywhere in Parch's first-party software: parchdroid and Parch-Driver-Manager are Python, and PKM (parch-kernel-manager) is a Python GTK app. This page covers the non-GTK side: a well-behaved command-line tool, its tests, its packaging, and the venv discipline that keeps your system Python intact. For the GTK side, see GTK apps.
Prerequisites
- A Parch or Arch-based system, working directory
~/devel, user privileges except wheresudois shown.
$ sudo pacman -S --needed python python-rich python-pytest python-build \
python-installer python-wheel python-setuptools
1. Create the project
$ mkdir -p parch-nag/src/parch_nag parch-nag/tests
$ cd parch-nag
$ touch src/parch_nag/__init__.py
parch-nag/
├── pyproject.toml
├── src/
│ └── parch_nag/
│ ├── __init__.py
│ └── cli.py
└── tests/
└── test_cli.py
pyproject.toml:
[project]
name = "parch-nag"
version = "0.1.0"
description = "Check whether packages are installed on Parch"
requires-python = ">=3.9"
dependencies = ["rich>=13"]
[project.scripts]
parch-nag = "parch_nag.cli:main"
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"
The [project.scripts] entry point is what puts a parch-nag command on
the user's PATH; the same mechanism PKM uses for its parch-kernel-manager
entry point.
2. Write the CLI
src/parch_nag/cli.py:
"""parch-nag: check whether packages are installed, politely."""
import argparse
import subprocess
from rich.console import Console
from rich.table import Table
console = Console()
err_console = Console(stderr=True)
def query_package(package: str) -> tuple[str, str]:
"""Return (status, version) for one pacman package name."""
result = subprocess.run(
["pacman", "-Qi", package],
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
return "missing", ""
version = ""
for line in result.stdout.splitlines():
if line.startswith("Version"):
version = line.split(":", 1)[1].strip()
break
return "installed", version
def parse_args(argv=None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog="parch-nag",
description="Check whether packages are installed on Parch",
)
parser.add_argument(
"-v", "--verbose", action="store_true",
help="log each pacman call to stderr")
parser.add_argument(
"packages", nargs="+", metavar="PACKAGE",
help="package names to check")
return parser.parse_args(argv)
def main(argv=None) -> int:
args = parse_args(argv)
missing = 0
table = Table(title="Parch package status")
table.add_column("Package")
table.add_column("Status")
table.add_column("Version")
for package in args.packages:
if args.verbose:
err_console.print(f"[dim]running pacman -Qi {package}[/dim]")
status, version = query_package(package)
style = "green" if status == "installed" else "red"
if status == "missing":
missing += 1
table.add_row(package, f"[{style}]{status}[/{style}]", version)
console.print(table)
return 1 if missing else 0
if __name__ == "__main__":
raise SystemExit(main())
The habits this file encodes: argparse with real help text, a single
exit code contract (0 for all installed, 1 when anything is missing, so
scripts can branch on it), stderr for diagnostics and stdout for results,
and rich for tables that degrade gracefully when output is piped.
3. Test it
tests/test_cli.py:
from parch_nag.cli import query_package
def test_missing_package_is_missing():
status, version = query_package("parch-nag-does-not-exist")
assert status == "missing"
assert version == ""
def test_base_kernel_is_installed():
status, version = query_package("linux")
assert status == "installed"
assert version # a version string always exists for an installed package
$ python3 -m venv .venv
$ . .venv/bin/activate
$ pip install -e .
$ pytest
$ parch-nag linux linux-firmware calamares-parch definitely-not-real
$ echo $?
Expected result: pytest passes, the tool prints a table with one red row
for the fake package, and the exit status is 1. The editable install runs
inside a venv, which is the same workflow PKM documents.
4. Package it
Follow the Arch Python packaging guidelines: build a wheel, install it with
python -m installer, and declare real dependencies instead of vendoring.
# Maintainer: Your Name <you@example.com>
pkgname=parch-nag
pkgver=0.1.0
pkgrel=1
pkgdesc="Check whether packages are installed on Parch"
arch=('any')
url="https://github.com/parchlinux/parch-nag"
license=('MIT')
depends=('python' 'python-rich')
makedepends=('python-build' 'python-installer' 'python-wheel' 'python-setuptools')
checkdepends=('python-pytest')
source=("$pkgname-$pkgver.tar.gz::https://github.com/parchlinux/parch-nag/archive/refs/tags/v$pkgver.tar.gz")
sha256sums=('SKIP') # fill with updpkgsums before any real submission
build() {
cd "$pkgname-$pkgver"
python -m build --wheel --no-isolation
}
check() {
cd "$pkgname-$pkgver"
pytest
}
package() {
cd "$pkgname-$pkgver"
python -m installer --destdir="$pkgdir" dist/*.whl
}
Notes on what the guidelines require:
dependsnames system packages (python-rich), never a bundled copy.python -m installerplaces theparch-nagentry point in/usr/binand the module under/usr/lib/python3.*/site-packages.--no-isolationuses the system build tools, so nothing is downloaded during the build.arch=('any')is correct for pure Python; usex86_64only if you ship compiled extensions.checkdependsruns your pytest suite during packaging, which catches breakage before it reaches users.
5. The venv and system Python rules
- Everything a user should keep: install through pacman or AUR, so the package manager owns it.
- Everything you are developing:
python -m venv .venv, activate it,pip install -e .inside it. Delete and recreate the venv freely. - Anything your package needs at runtime: declare it in
dependswith thepython-prefix. Do not vendor wheels, do not shipsite-packagescopies, do not addpipto a package. - Tools that must run outside a venv (
pipx, oruv tool install) are a user choice for dev utilities, never a packaging strategy for the repositories. - Test against the Python version Parch ships, not a venv pin: check
pacman -Q pythonbefore settingrequires-python.
6. Desktop integration for CLI tools
A CLI is still a desktop citizen:
- Ship a man page or at least
--helpthat stands alone. - Keep diagnostics on stderr so journald capture (for anything running under a systemd unit) stays clean.
- Register a desktop entry only if the tool opens a window or a terminal; see desktop integration.
- Translate user-facing strings like any other app, see localization.
Related pages
- GTK apps in Python: the full libadwaita tutorial.
- Rust apps: when a compiled binary fits better.
- PKGBUILD reference: every variable and function used above.
- Security practices: dependency hygiene and why the vendoring ban matters.
- HIG: if your tool grows a GUI.