Skip to main content

Introspect Typer apps and export their full command manifest with pluggable renderers.

Project description

typer-manifest

PyPI version Python versions License Code style: black

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:

  1. Collector (collect_manifest): Walks your CLI app and returns a pure Python dictionary
  2. 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 object
  • root_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 object
  • path: File path where the JSON manifest should be written
  • root_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 by build_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

  • Typer - The CLI framework this tool introspects
  • Click - The underlying library for Typer

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

typer_manifest-0.2.1.tar.gz (20.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

typer_manifest-0.2.1-py3-none-any.whl (17.7 kB view details)

Uploaded Python 3

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

Hashes for typer_manifest-0.2.1.tar.gz
Algorithm Hash digest
SHA256 697eabbeba1d744e3505ce9c3b364d5cca6be1042d17549159a76e90cb87eae2
MD5 6e7486b0f12840baf2e2172b8f3619d5
BLAKE2b-256 ffcae82b08c3bc457eae3b0c40f1488fcb7d31bcba1157467c96e9fc5adbda34

See more details on using hashes here.

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

Hashes for typer_manifest-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 03068c8d7b0a727f5cdf854f57f665e5975b658468b605899a4bb32ebc7e2650
MD5 f3a96ac5b2eef3c47194a94f05f917b8
BLAKE2b-256 fe7373201919c8e888bd563c46f7940c681ff9e00d6ae3de9c7b8c27c83510fe

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page