clan-core/pkgs/clan-cli/docs.py
a-kenji 7078f09872
All checks were successful
buildbot/nix-build .#checks.aarch64-darwin.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-flash-installer Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-iso-installer Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-flash-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-webview-ui Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-bash Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-ts-api Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-cli-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-e2fsprogs Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.package-module-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-openssh Build done.
buildbot/nix-build .#checks.x86_64-linux.renderClanOptions Build done.
buildbot/nix-build .#checks.x86_64-linux."clan-dep-python3.11-mypy" Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-rsync Build done.
buildbot/nix-build .#checks.x86_64-linux.package-default Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-fakeroot Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-git Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-nix Build done.
buildbot/nix-build .#checks.x86_64-linux."clan-dep-python3.11-qemu" Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-sops Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-test-backup Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-sshpass Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-tor Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-test-backup Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-zbar Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-vm-manager-no-breakpoints Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-iso-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-cli Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-age Build done.
buildbot/nix-build .#checks.x86_64-linux.check-for-breakpoints Build done.
buildbot/nix-build .#checks.x86_64-linux.borgbackup Build done.
buildbot/nix-build .#checks.x86_64-linux.container Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-default Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-deb Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-rpm Build done.
buildbot/nix-build .#checks.x86_64-linux.package-impure-checks Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.x86_64-linux.treefmt Build done.
buildbot/nix-build .#checks.x86_64-linux.package-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.package-merge-after-ci Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-vm-manager-pytest Build done.
buildbot/nix-build .#checks.x86_64-linux.lib-jsonschema-nix-unit-tests Build done.
buildbot/nix-build .#checks.x86_64-linux.package-moonlight-sunshine-accept Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-clan-cli Build done.
buildbot/nix-build .#checks.x86_64-linux.lib-jsonschema-example-valid Build done.
buildbot/nix-build .#checks.x86_64-linux.deltachat Build done.
buildbot/nix-build .#checks.x86_64-linux.matrix-synapse Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-test-backup Build done.
buildbot/nix-build .#checks.x86_64-linux.package-editor Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-apk Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-archlinux Build done.
buildbot/nix-build .#checks.x86_64-linux.package-wayland-proxy-virtwl Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zerotier-members Build done.
buildbot/nix-build .#checks.x86_64-linux.test-backups Build done.
buildbot/nix-build .#checks.x86_64-linux.package-pending-reviews Build done.
buildbot/nix-build .#checks.x86_64-linux.package-tea-create-pr Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zerotierone Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zt-tcp-relay Build done.
buildbot/nix-build .#checks.x86_64-linux.module-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.package-function-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-clan-vm-manager Build done.
buildbot/nix-build .#checks.x86_64-linux.package-module-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.secrets Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-flash-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.package-deploy-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.zt-tcp-relay Build done.
buildbot/nix-build .#checks.x86_64-linux.wayland-proxy-virtwl Build done.
buildbot/nix-build .#checks.x86_64-linux.package-iso-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-iso-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.syncthing Build done.
buildbot/nix-build .#checks.x86_64-linux.package-webview-ui Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-vm-manager Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-pytest-with-core Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-pytest-without-core Build done.
checks / checks-impure (pull_request) Successful in 2m41s
buildbot/nix-build .#checks.x86_64-linux.test-installation Build done.
buildbot/nix-eval Build done.
clan/docs.py: remove epilog from the reference overview
2024-05-28 20:01:48 +02:00

389 lines
12 KiB
Python

import argparse
import sys
from dataclasses import dataclass
from pathlib import Path
from clan_cli import create_parser
@dataclass
class Option:
name: str
description: str
default: str | None = None
metavar: str | None = None
epilog: str | None = None
def to_md_li(self, delim: str = "-") -> str:
# - **--example, `--e`**: <PATH> {description} (Default: `default` {epilog})
md_li = f"{delim} **{self.name}**: "
md_li += f"`<{self.metavar}>` " if self.metavar else ""
md_li += f"(Default: `{self.default}`) " if self.default else ""
md_li += indent_next(
f"\n{self.description.strip()}" if self.description else ""
)
# md_li += indent_next(f"\n{self.epilog.strip()}" if self.epilog else "")
return md_li
@dataclass
class Subcommand:
name: str
description: str | None = None
epilog: str | None = None
def to_md_li(self, parent: "Category") -> str:
md_li = f"""- **[{self.name}](#{"-".join(parent.title.split(" "))}-{self.name})**: """
md_li += indent_next(f"{self.description.strip()} " if self.description else "")
md_li += indent_next(f"\n{self.epilog.strip()}" if self.epilog else "")
return md_li
icon_table = {
"backups": ":material-backup-restore: ",
"config": ":material-shape-outline: ",
"facts": ":simple-databricks: ",
"flakes": ":material-snowflake: ",
"flash": ":material-flash: ",
"history": ":octicons-history-24: ",
"machines": ":octicons-devices-24: ",
"secrets": ":octicons-passkey-fill-24: ",
"ssh": ":material-ssh: ",
"vms": ":simple-virtualbox: ",
}
@dataclass
class Category:
title: str
# Flags such as --example, -e
options: list[Option]
# Positionals such as 'cmd <example>'
positionals: list[Option]
# Subcommands such as clan 'machines'
# In contrast to an option it is a command that can have further children
subcommands: list[Subcommand]
# Description of the command
description: str | None = None
# Additional information, typically displayed at the bottom
epilog: str | None = None
# What level of depth the category is at (i.e. 'backups list' is 2, 'backups' is 1, 'clan' is 0)
level: int = 0
def to_md_li(self, level: int = 1) -> str:
md_li = ""
if level == self.level:
icon = icon_table.get(self.title, "")
md_li += f"""- **[{icon}{self.title}](./{"-".join(self.title.split(" "))}.md)**\n\n"""
md_li += f"""{indent_all("---", 4)}\n\n"""
md_li += indent_all(
f"{self.description.strip()}\n" if self.description else "", 4
)
return md_li
def epilog_to_md(text: str) -> str:
"""
Convert the epilog to md
"""
after_examples = False
md = ""
# md += text
for line in text.split("\n"):
if line.strip() == "Examples:":
after_examples = True
md += "### Examples"
md += "\n"
else:
if after_examples:
if line.strip().startswith("$"):
md += f"`{line}`"
md += "\n"
md += "\n"
else:
if contains_https_link(line):
line = convert_to_markdown_link(line)
md += line
md += "\n"
else:
md += line
md += "\n"
return md
import re
def contains_https_link(line: str) -> bool:
pattern = r"https://\S+"
return re.search(pattern, line) is not None
def convert_to_markdown_link(line: str) -> str:
pattern = r"(https://\S+)"
# Replacement pattern to convert it to a Markdown link
return re.sub(pattern, r"[\1](\1)", line)
def indent_next(text: str, indent_size: int = 4) -> str:
"""
Indent all lines in a string except the first line.
This is useful for adding multiline texts a lists in Markdown.
"""
indent = " " * indent_size
lines = text.split("\n")
indented_text = lines[0] + ("\n" + indent).join(lines[1:])
return indented_text
def indent_all(text: str, indent_size: int = 4) -> str:
"""
Indent all lines in a string.
"""
indent = " " * indent_size
lines = text.split("\n")
indented_text = indent + ("\n" + indent).join(lines)
return indented_text
def get_subcommands(
parser: argparse.ArgumentParser,
to: list[Category],
level: int = 0,
prefix: list[str] = [],
) -> tuple[list[Option], list[Option], list[Subcommand]]:
"""
Generate Markdown documentation for an argparse.ArgumentParser instance including its subcommands.
:param parser: The argparse.ArgumentParser instance.
:param level: Current depth of subcommand.
:return: Markdown formatted documentation as a string.
"""
# Document each argument
# --flake --option --debug, etc.
flag_options: list[Option] = []
positional_options: list[Option] = []
subcommands: list[Subcommand] = []
for action in parser._actions:
if isinstance(action, argparse._HelpAction):
# Pseudoaction that holds the help message
continue
if isinstance(action, argparse._SubParsersAction):
continue # Subparsers handled separately
option_strings = ", ".join(action.option_strings)
if option_strings:
flag_options.append(
Option(
name=option_strings,
description=action.help if action.help else "",
default=action.default if action.default is not None else "",
metavar=f"{action.metavar}" if action.metavar else "",
)
)
if not option_strings:
# Positional arguments
positional_options.append(
Option(
name=action.dest,
description=action.help if action.help else "",
default=action.default if action.default is not None else "",
metavar=f"{action.metavar}" if action.metavar else "",
)
)
for action in parser._actions:
if isinstance(action, argparse._SubParsersAction):
subparsers: dict[str, argparse.ArgumentParser] = action.choices
for name, subparser in subparsers.items():
parent = " ".join(prefix)
sub_command = Subcommand(name=name, description=subparser.description)
subcommands.append(sub_command)
(_options, _positionals, _subcommands) = get_subcommands(
parser=subparser, to=to, level=level + 1, prefix=[*prefix, name]
)
to.append(
Category(
title=f"{parent} {name}",
description=subparser.description,
# epilog=subparser.epilog,
level=level,
options=_options,
positionals=_positionals,
subcommands=_subcommands,
)
)
return (flag_options, positional_options, subcommands)
def collect_commands() -> list[Category]:
"""
Returns a sorted list of all available commands.
i.e.
a...
backups
backups create
backups list
backups restore
c...
Commands are sorted alphabetically and kept in groups.
"""
parser = create_parser()
result: list[Category] = []
for action in parser._actions:
if isinstance(action, argparse._SubParsersAction):
subparsers: dict[str, argparse.ArgumentParser] = action.choices
for name, subparser in subparsers.items():
(_options, _positionals, _subcommands) = get_subcommands(
subparser, to=result, level=2, prefix=[name]
)
result.append(
Category(
title=name,
description=subparser.description,
options=_options,
positionals=_positionals,
subcommands=_subcommands,
epilog=subparser.epilog,
level=1,
)
)
def weight_cmd_groups(c: Category) -> tuple[str, str, int]:
sub = [o for o in result if o.title.startswith(c.title) and o.title != c.title]
weight = 10 - len(c.title.split(" "))
if sub:
weight = 10 - len(sub[0].title.split(" "))
# 1. Sort by toplevel name alphabetically
# 2. sort by custom weight to keep groups together
# 3. sort by title alphabetically
return (c.title.split(" ")[0], c.title, weight)
result = sorted(result, key=weight_cmd_groups)
return result
def build_command_reference() -> None:
"""
Function that will build the reference
and write it to the out path.
"""
cmds = collect_commands()
folder = Path("out")
folder.mkdir(parents=True, exist_ok=True)
# Index file
markdown = "# CLI Overview\n\n"
categories_fmt = ""
for cat in cmds:
categories_fmt += f"{cat.to_md_li()}\n\n" if cat.to_md_li() else ""
if categories_fmt:
markdown += """## Overview\n\n"""
markdown += '<div class="grid cards" markdown>\n\n'
markdown += categories_fmt
markdown += "</div>"
markdown += "\n"
with open(folder / "index.md", "w") as f:
f.write(markdown)
# Each top level category is a separate file
files: dict[Path, str] = {}
for t in [cmd.title for cmd in cmds]:
print(t)
for cmd in cmds:
# Collect all commands starting with the same name into one file
filename = cmd.title.split(" ")[0]
markdown = files.get(folder / f"{filename}.md", "")
markdown += f"{'#'*(cmd.level)} {cmd.title.capitalize()}\n\n"
markdown += f"{cmd.description}\n\n" if cmd.description else ""
# usage: clan vms run [-h] machine
markdown += f"""Usage: `clan {cmd.title}`\n\n"""
# options:
# -h, --help show this help message and exit
# Positional arguments
positionals_fmt = ""
for option in cmd.positionals:
positionals_fmt += f"""{option.to_md_li("1.")}\n"""
if len(cmd.positionals):
markdown += """!!! info "Positional arguments"\n"""
markdown += indent_all(positionals_fmt)
markdown += "\n"
options_fmt = ""
for option in cmd.options:
options_fmt += f"{option.to_md_li()}\n"
# options:
if len(cmd.options):
markdown += """??? info "Options"\n"""
markdown += indent_all(options_fmt)
markdown += "\n"
def asort(s: Subcommand) -> str:
return s.name
commands_fmt = ""
for sub_cmd in sorted(cmd.subcommands, key=asort):
commands_fmt += f"{sub_cmd.to_md_li(cmd)}\n"
if commands_fmt:
markdown += """!!! info "Commands"\n"""
markdown += indent_all(commands_fmt)
markdown += "\n"
markdown += f"{epilog_to_md(cmd.epilog)}\n\n" if cmd.epilog else ""
files[folder / f"{filename}.md"] = markdown
for fname, content in files.items():
with open(fname, "w") as f:
f.write(content)
def main() -> None:
if len(sys.argv) != 2:
print("Usage: python docs.py <command>")
print("Available commands: reference")
sys.exit(1)
command = sys.argv[1]
if command == "reference":
build_command_reference()
if __name__ == "__main__":
main()