File size: 7,851 Bytes
44459bb |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
import re
import types
from typing import Union, get_args, get_origin, get_type_hints
import typer
from folding_studio.cli import app
from typer.models import TyperInfo
from typer.utils import get_params_from_function
def extract_base_type(annotation):
"""Extract annotation type.
Handles List[X], Optional[X] and Union[X, Y] cases.
"""
origin = get_origin(annotation)
if origin is list:
inner_type = get_args(annotation)[0]
return (
f"List[{inner_type.__name__}]"
if hasattr(inner_type, "__name__")
else "List[Unknown]"
)
if origin is Union or isinstance(origin, types.UnionType):
inner_types = [t.__name__ for t in get_args(annotation) if t is not type(None)]
return " | ".join(inner_types) if inner_types else "Unknown"
if isinstance(annotation, types.UnionType):
inner_types = [t.__name__ for t in get_args(annotation) if t is not type(None)]
return " | ".join(inner_types) if inner_types else "Unknown"
return annotation.__name__ if hasattr(annotation, "__name__") else "Unknown"
def update_params_with_default(param_info: dict[str, str]) -> dict[str, str]:
"""
Update the 'default' value in the params dictionary based on its description.
Args:
param_info (dict): A dictionary describing a parameter.
Returns:
dict: The updated param_info dictionary.
"""
if param_info.get("default") != "No default":
return param_info
description = param_info.get("description", "")
# Regular expression to find a sentence starting with 'Default to ' and ending with a period
match = re.search(r"Default to '([^']+)'", description)
if not match:
return param_info
# Extract the default value
default_value = match.group(1)
# Update the 'description' by removing the 'Default to ' phrase
param_info["description"] = re.sub(
r"Default to '[^']+'\.\s*", "", description
).strip()
# Ensure the description ends with a single period
if not param_info["description"].endswith("."):
param_info["description"] += "."
# Update the 'default' key with the extracted value
param_info["default"] = default_value
param_info["default"] = param_info["default"].replace("<", "<")
param_info["default"] = param_info["default"].replace(">", ">")
return param_info
def extract_command_info(command: typer.models.CommandInfo):
func = command.callback
parameters = get_params_from_function(func)
hints = get_type_hints(func)
docstring = func.__doc__ or "No docstring provided."
docstring = docstring.replace("\n", " ").replace("\n\n", " ")
docstring = re.sub(r"(https?://\S+)", r"<\1>", docstring)
command_info = {
"docstring": docstring,
"name": command.name if command.name else func.__name__,
"params": [],
"options": [],
}
for name, param in parameters.items():
raw_type = hints.get(name, None)
base_type = extract_base_type(raw_type)
description = str(param.default.help)
description = description.replace("\n", " ").replace("\n\n", " ")
param_info = {
"name": name,
"type": base_type,
"description": description,
}
if isinstance(param.default, typer.models.OptionInfo):
name = name.replace("_", "-")
if base_type == "bool":
name = name + f" / --no-{name}"
name = f"--{name}"
param_info["name"] = name
default_value = param.default.default
if param_info["type"] == "bool":
values = param_info["name"].split(" / ")
default_value = values[0] if param.default.default else values[1]
if default_value is Ellipsis:
default_value = "No default"
param_info["default"] = default_value
# if No default, check if default value is in description
param_info = update_params_with_default(param_info)
command_info["options"].append(param_info)
else:
command_info["params"].append(param_info)
return command_info
def generate_markdown_level_docs(
f, group: TyperInfo, cli_name: str, level=1, base_name=None
):
base_level = "#" * level
f.write(f"{base_level} `{base_name + ' ' if base_name else ''}{group.name}`\n")
for subcommand in group.typer_instance.registered_commands:
subcommand_info = extract_command_info(subcommand)
subcommand_name = (
subcommand.name
if subcommand.name is not None
else subcommand.callback.__name__
)
command_name = (
f"{base_name + ' ' if base_name else ''}{group.name} {subcommand_name}"
)
f.write(f"{base_level}# `{command_name}`\n\n")
f.write(f"{subcommand_info['docstring']}\n\n")
usage = "**Usage**:\n\n"
usage += "```console\n"
usage += f"{cli_name} {command_name}{' [OPTIONS]' if subcommand_info['options'] else ''}"
if subcommand_info["params"]:
usage += f" {' '.join(param['name'].upper() for param in subcommand_info['params'])}"
usage += "\n```\n\n"
f.write(usage)
if subcommand_info["params"]:
# Arguments
f.write("**Arguments**:\n\n")
f.write("| ARGUMENT | DESCRIPTION | VALUE TYPE |\n")
f.write("| -------- | ----------- | ----------- |\n")
for param in subcommand_info["params"]:
param["description"] = (
param["description"]
if param.get("description")
else "No description"
)
param["type"] = param["type"] if param["type"] else "No type"
f.write(
f"| {param['name'].upper()} | {param['description']} | {param['type']} |\n"
)
f.write("\n")
# Options
if subcommand_info["options"]:
f.write("**Options**:\n\n")
f.write("| OPTIONS | DESCRIPTION | VALUE TYPE | DEFAULT VALUE |\n")
f.write("| ------- | ----------- | ---------- | ------------- |\n")
for param in subcommand_info["options"]:
param["description"] = (
param["description"]
if param.get("description")
else "No description"
)
param["type"] = param["type"] if param["type"] else "No type"
param["default"] = (
param["default"] if param["default"] is not None else "No default"
)
# if No default, check if default value is in description
param = update_params_with_default(param)
f.write(
f"| {param['name']} | {param['description']} | {param['type']} | {param['default']} |\n"
)
f.write("\n")
for subgroup in group.typer_instance.registered_groups:
generate_markdown_level_docs(f, subgroup, cli_name, level + 1, group.name)
def generate_markdown_docs() -> None:
"""
Generate markdown documentation for all registered commands and subcommands in the application.
The documentation will include descriptions, arguments, and options.
The generated markdown is saved in the 'docs/reference/cli.md' file.
"""
# Iterate through each group in app.registered_groups
with open("docs/reference/cli.md", "w") as f:
for group in app.registered_groups:
if group.name == "key":
continue
generate_markdown_level_docs(f, group, "folding", 2)
if __name__ == "__main__":
generate_markdown_docs()
|