Introspect Typer apps and export their full command manifest with pluggable renderers.
Project description
typer-manifest
Export Typer CLI structures as machine-readable manifests.
This lightweight utility walks a Typer (Click-based) application and emits a JSON document describing all commands, subcommands, and parameters — useful for documentation, testing, and automated CLI introspection.
Features
- 🔌 Pluggable Renderers: Separate collection from rendering for maximum flexibility
- 📋 JSON Export: Generate complete CLI structure as JSON
- 📝 Markdown Rendering: Create human-readable documentation
- 🎨 Custom Formats: Use functions, classes, or templates for custom output
- 🔍 Deep Introspection: Captures commands, subcommands, parameters, types, defaults, and help text
- ⚡ Typer & Click Support: Works with both Typer and Click applications
- 🎯 Fully Typed: Complete type annotations for better IDE support
- 🪶 Zero Extra Dependencies: Core stays lightweight, integrate Jinja2/YAML in your project
Installation
pip install typer-manifest
Quick Start
Basic Usage
from typer_manifest import build_manifest, write_manifest
import typer
app = typer.Typer()
@app.command()
def hello(name: str = typer.Option("World", help="Name to greet")):
"""Say hello to someone."""
print(f"Hello {name}!")
@app.command()
def goodbye(name: str = typer.Option("World", help="Name to say goodbye to")):
"""Say goodbye to someone."""
print(f"Goodbye {name}!")
# Generate manifest programmatically
manifest = build_manifest(app, root_command_name="myapp")
print(manifest)
# Or write directly to a file
write_manifest(app, "docs/cli-manifest.json", root_command_name="myapp")
Output Format
The generated JSON manifest looks like:
{
"name": "myapp",
"commands": [
{
"name": "hello",
"path": "myapp hello",
"help": "Say hello to someone.",
"params": [
{
"name": "name",
"opts": ["--name"],
"help": "Name to greet",
"required": false,
"default": "World",
"type": "option"
}
],
"commands": []
},
{
"name": "goodbye",
"path": "myapp goodbye",
"help": "Say goodbye to someone.",
"params": [
{
"name": "name",
"opts": ["--name"],
"help": "Name to say goodbye to",
"required": false,
"default": "World",
"type": "option"
}
],
"commands": []
}
]
}
Markdown Documentation
Generate human-readable markdown documentation:
from typer_manifest import build_manifest, render_manifest_list
manifest = build_manifest(app, root_command_name="myapp")
markdown = render_manifest_list(manifest)
print(markdown)
Output:
# myapp commands
- myapp hello: Say hello to someone.
- myapp goodbye: Say goodbye to someone.
Pluggable Renderers
New in v0.2.0: typer-manifest now separates collection (introspection) from rendering (output formatting), giving you complete control over output formats.
Architecture
The package is organized into two independent layers:
- Collector (
collect_manifest): Walks your CLI app and returns a pure Python dictionary - Renderers: Transform the manifest dict into various output formats
New API (Recommended)
from typer_manifest import collect_manifest, render_manifest
# Collect once
manifest = collect_manifest(app, "myapp")
# Render in multiple formats
json_output = render_manifest(manifest, format="json")
markdown_output = render_manifest(manifest, format="markdown")
compact_output = render_manifest(manifest, format="compact")
Built-in Renderers
JsonRenderer - Formatted JSON (customizable indentation):
from typer_manifest import JsonRenderer, collect_manifest
manifest = collect_manifest(app)
renderer = JsonRenderer(indent=4, sort_keys=True)
output = renderer(manifest)
MarkdownRenderer - Hierarchical bullet list with help text:
from typer_manifest import MarkdownRenderer
renderer = MarkdownRenderer()
output = renderer(manifest)
# Output:
# # myapp commands
# - myapp hello: Say hello
# - myapp goodbye: Say goodbye
CompactMarkdownRenderer - Command paths only, no help text:
from typer_manifest import CompactMarkdownRenderer
renderer = CompactMarkdownRenderer()
output = renderer(manifest)
# Output:
# # myapp commands
# - myapp hello
# - myapp goodbye
Custom Renderers
Option 1: Simple Function
def my_custom_renderer(manifest):
lines = [f"CLI: {manifest['name']}", "Commands:"]
for cmd in manifest['commands']:
lines.append(f" • {cmd['name']}")
return "\n".join(lines)
manifest = collect_manifest(app)
output = render_manifest(manifest, renderer=my_custom_renderer)
Option 2: Renderer Class
class TableRenderer:
"""Render as a simple table."""
def __call__(self, manifest):
lines = ["Command | Help", "--------|-----"]
for cmd in manifest['commands']:
lines.append(f"{cmd['name']} | {cmd['help']}")
return "\n".join(lines)
output = render_manifest(manifest, renderer=TableRenderer())
Option 3: Template String
template = """
# CLI Documentation
Application: {{ manifest }}
Generated automatically.
"""
output = render_manifest(manifest, template=template)
User-Space Integration (No Dependencies Required!)
The core package stays lightweight with zero extra dependencies. For advanced templating, add your preferred libraries:
With Jinja2:
from jinja2 import Template
from typer_manifest import collect_manifest
manifest = collect_manifest(app)
template = Template("""
# {{ manifest.name }} CLI Reference
{% for cmd in manifest.commands %}
## {{ cmd.path }}
{{ cmd.help }}
**Parameters:**
{% for param in cmd.params %}
- `{{ param.name }}`: {{ param.help }} (default: {{ param.default }})
{% endfor %}
{% endfor %}
""")
print(template.render(manifest=manifest))
With YAML:
import yaml
from typer_manifest import collect_manifest
manifest = collect_manifest(app)
# Custom YAML renderer
def yaml_renderer(m):
return yaml.dump(m, default_flow_style=False)
output = render_manifest(manifest, renderer=yaml_renderer)
With TOML:
import toml
from typer_manifest import collect_manifest
manifest = collect_manifest(app)
def toml_renderer(m):
return toml.dumps(m)
output = render_manifest(manifest, renderer=toml_renderer)
Renderer Protocol
Any callable that accepts a Manifest dict and returns a string is a valid renderer:
from typing import Protocol
from typer_manifest import Manifest
class Renderer(Protocol):
def __call__(self, manifest: Manifest) -> str:
...
This means functions, classes with __call__, lambdas, and any callable work automatically!
Migration from Old API
The old API still works for backward compatibility:
# Old API (still supported)
from typer_manifest import build_manifest, render_manifest_list
manifest = build_manifest(app, "myapp")
markdown = render_manifest_list(manifest)
New code should use:
# New API (recommended)
from typer_manifest import collect_manifest, render_manifest
manifest = collect_manifest(app, "myapp")
markdown = render_manifest(manifest, format="markdown")
Advanced Usage
Nested Command Groups
import click
@click.group()
def cli():
"""Database management CLI."""
pass
@cli.group()
def db():
"""Database commands."""
pass
@db.command()
def migrate():
"""Run database migrations."""
pass
@db.command()
def seed():
"""Seed the database."""
pass
from typer_manifest import build_manifest
manifest = build_manifest(cli, root_command_name="myapp")
# Captures full hierarchy: myapp -> db -> migrate/seed
Click Applications
typer-manifest works seamlessly with Click applications too:
import click
from typer_manifest import write_manifest
@click.command()
@click.option('--count', default=1, help='Number of greetings')
@click.option('--name', prompt='Your name', help='The person to greet')
def hello(count, name):
"""Simple program that greets NAME COUNT times."""
for _ in range(count):
click.echo(f'Hello, {name}!')
write_manifest(hello, "cli-manifest.json", root_command_name="hello")
API Reference
build_manifest(app, root_command_name=None)
Introspect a Click or Typer app and return a structured manifest.
Parameters:
app: A Typer app or Click command objectroot_command_name(optional): Name for the root command. If not provided, attempts to derive from the command's name attribute
Returns: Dictionary containing the complete command hierarchy
write_manifest(app, path, root_command_name=None)
Generate a manifest and write it to a JSON file.
Parameters:
app: A Typer app or Click command objectpath: File path where the JSON manifest should be writtenroot_command_name(optional): Name for the root command
render_manifest_list(manifest)
Render a manifest as a Markdown bullet list.
Parameters:
manifest: A manifest dictionary generated bybuild_manifest()
Returns: Markdown-formatted string with a hierarchical bullet list
Use Cases
Documentation Generation
Automatically generate CLI documentation for your projects:
from typer_manifest import build_manifest, render_manifest_list
from pathlib import Path
manifest = build_manifest(app, "myapp")
markdown = render_manifest_list(manifest)
Path("docs/cli-reference.md").write_text(markdown)
Testing
Validate your CLI structure in tests:
def test_cli_structure():
manifest = build_manifest(app, "myapp")
# Ensure all expected commands exist
command_names = [cmd["name"] for cmd in manifest["commands"]]
assert "init" in command_names
assert "build" in command_names
# Validate command parameters
build_cmd = next(c for c in manifest["commands"] if c["name"] == "build")
param_names = [p["name"] for p in build_cmd["params"]]
assert "output" in param_names
CI/CD Integration
Track CLI changes over time by committing manifests:
# generate_manifest.py
from my_cli import app
from typer_manifest import write_manifest
write_manifest(app, "cli-manifest.json", "mycli")
Then in CI:
python generate_manifest.py
git diff --exit-code cli-manifest.json || echo "CLI structure changed!"
Development
Setup
# Clone the repository
git clone https://github.com/cprima-forge/typer-manifest
cd typer-manifest
# Install with development dependencies
uv pip install -e ".[dev]"
Running Tests
pytest tests/ -v
Type Checking
mypy src/typer_manifest
Code Formatting
black src/ tests/
ruff check src/ tests/
Requirements
- Python >=3.10
- typer >=0.12
- click >=8.1
License
Copyright 2024 Christian Prior-Mamulyan
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
See CHANGELOG.md for version history.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Related Projects
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file typer_manifest-0.2.1.tar.gz.
File metadata
- Download URL: typer_manifest-0.2.1.tar.gz
- Upload date:
- Size: 20.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
697eabbeba1d744e3505ce9c3b364d5cca6be1042d17549159a76e90cb87eae2
|
|
| MD5 |
6e7486b0f12840baf2e2172b8f3619d5
|
|
| BLAKE2b-256 |
ffcae82b08c3bc457eae3b0c40f1488fcb7d31bcba1157467c96e9fc5adbda34
|
File details
Details for the file typer_manifest-0.2.1-py3-none-any.whl.
File metadata
- Download URL: typer_manifest-0.2.1-py3-none-any.whl
- Upload date:
- Size: 17.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
03068c8d7b0a727f5cdf854f57f665e5975b658468b605899a4bb32ebc7e2650
|
|
| MD5 |
f3a96ac5b2eef3c47194a94f05f917b8
|
|
| BLAKE2b-256 |
fe7373201919c8e888bd563c46f7940c681ff9e00d6ae3de9c7b8c27c83510fe
|