Manifest Compiler ================= :meth:`~xclif.Cli.from_routes` discovers commands by walking your routes package with ``pkgutil.walk_packages`` on every invocation. For most CLIs this is imperceptible, but it adds ~13–15 ms of cold-start overhead on Apple Silicon — enough to matter if your tool is invoked hundreds of times per second (e.g. shell completion hooks or CI scripts). The **manifest compiler** eliminates this overhead. It walks the package *once* at build time and writes a static ``_xclif_manifest.py`` file. At runtime, loading the manifest is a plain Python import — no filesystem walk, no ``inspect.getmembers``. When to use it -------------- Consider compiling a manifest when: * Shell completion feels sluggish, because the completion hook spawns your CLI on every keypress. * Your tool is called in tight loops (CI scripts, ``git`` hooks, makefiles). * You ship a compiled package (wheel) and want the fastest possible startup. For interactive development, ``from_routes`` is easier — no extra build step, and route changes are picked up automatically. Compiling a manifest -------------------- **From the command line** (recommended): .. code-block:: bash python -m xclif compile myapp.routes This imports ``myapp.routes`` and writes ``_xclif_manifest.py`` next to the routes package (i.e. inside the ``myapp/`` directory). Pass ``--output `` to write elsewhere: .. code-block:: bash python -m xclif compile myapp.routes --output src/myapp **Programmatically** (e.g. in a build script): .. code-block:: python from xclif.compiler import compile_routes import myapp.routes as routes path = compile_routes(routes) print(f"Manifest written to {path}") Either way, commit the generated file to version control so it is available in installed distributions without a separate compilation step. Loading the manifest at runtime -------------------------------- Replace :meth:`~xclif.Cli.from_routes` with :meth:`~xclif.Cli.from_manifest` in your entry point: .. code-block:: python # myapp/__main__.py from xclif import Cli from myapp import _xclif_manifest cli = Cli.from_manifest(_xclif_manifest) if __name__ == "__main__": cli() :meth:`~xclif.Cli.from_manifest` calls the ``_build_cli()`` function inside the manifest module, which lazily imports each route and assembles the command tree — exactly the same tree that ``from_routes`` would produce. What the manifest looks like ----------------------------- The generated ``_xclif_manifest.py`` is readable Python. Given a routes package with this layout: .. code-block:: text myapp/routes/ ├── __init__.py → myapp (root) ├── greet.py → myapp greet └── server/ ├── __init__.py → myapp server (group) ├── start.py → myapp server start └── stop.py → myapp server stop The compiler writes: .. code-block:: python # Generated by `xclif compile` — do not edit by hand. # Re-run `python -m xclif compile ` after adding/removing routes. from __future__ import annotations from xclif import Cli def _build_cli(version: str | None = None, env_prefix: str | None = None, config_name: str | None = None, local_config: str | None = None) -> Cli: from myapp.routes import _ as _root from myapp.routes.greet import _ as _myapp_routes_greet from myapp.routes.server import _ as _myapp_routes_server from myapp.routes.server.start import _ as _myapp_routes_server_start from myapp.routes.server.stop import _ as _myapp_routes_server_stop root = _root cli = Cli(root_command=root, version=version, env_prefix=env_prefix, config_name=config_name, local_config=local_config) cli.add_command(['server'], _myapp_routes_server) cli.add_command(['greet'], _myapp_routes_greet) cli.add_command(['server', 'start'], _myapp_routes_server_start) cli.add_command(['server', 'stop'], _myapp_routes_server_stop) cli._finalize() return cli The imports are **inside** ``_build_cli()`` so loading the manifest module itself is free — route modules are only imported when you call ``_build_cli()``, matching the laziness of ``from_routes``. Keeping the manifest up to date -------------------------------- Re-run ``xclif compile`` after **adding, removing, or renaming** any route file. The manifest does not self-update. A convenient place to hook this in is your build system. For example, with ``pyproject.toml`` and `hatch `_: .. code-block:: toml [tool.hatch.build.hooks.custom] # runs `python -m xclif compile myapp.routes` before building the wheel Or add a ``Makefile`` target: .. code-block:: makefile manifest: python -m xclif compile myapp.routes If you use a CI pipeline, run the compile step before running tests so the manifest under test matches the current routes. .. tip:: Run ``python -m xclif compile --help`` to see all available options. API Reference ------------- .. automodule:: xclif.compiler :members: :show-inheritance: