-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Problem command description being shortened
In the generated doc the sub command description were being shortened, for example: allocate: Notify a CRN to start an instance (for... confidential: Create (optional), start and unlock a... Solution: Vendroize and customize the tool to generate the doc Switch the char limit from 45 to 1000 It will still only take the first paragraph
- Loading branch information
Showing
2 changed files
with
281 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,280 @@ | ||
#!/usr/bin/env python3 | ||
""" | ||
Copied from typer.cli.py to customise doc generation | ||
""" | ||
|
||
import click | ||
import importlib.util | ||
import re | ||
import sys | ||
import typer | ||
import typer.core | ||
from click import Command, Group | ||
from pathlib import Path | ||
from typing import Any, List, Optional | ||
|
||
default_app_names = ("app", "cli", "main") | ||
default_func_names = ("main", "cli", "app") | ||
|
||
utils_app = typer.Typer(help="Extra utility commands for Typer apps.") | ||
|
||
|
||
class State: | ||
def __init__(self) -> None: | ||
self.app: Optional[str] = None | ||
self.func: Optional[str] = None | ||
self.file: Optional[Path] = None | ||
self.module: Optional[str] = None | ||
|
||
|
||
state = State() | ||
|
||
|
||
def maybe_update_state(ctx: click.Context) -> None: | ||
path_or_module = ctx.params.get("path_or_module") | ||
if path_or_module: | ||
file_path = Path(path_or_module) | ||
if file_path.exists() and file_path.is_file(): | ||
state.file = file_path | ||
else: | ||
if not re.fullmatch(r"[a-zA-Z_]\w*(\.[a-zA-Z_]\w*)*", path_or_module): | ||
typer.echo(f"Not a valid file or Python module: {path_or_module}", err=True) | ||
sys.exit(1) | ||
state.module = path_or_module | ||
app_name = ctx.params.get("app") | ||
if app_name: | ||
state.app = app_name | ||
func_name = ctx.params.get("func") | ||
if func_name: | ||
state.func = func_name | ||
|
||
|
||
class TyperCLIGroup(typer.core.TyperGroup): | ||
def list_commands(self, ctx: click.Context) -> List[str]: | ||
self.maybe_add_run(ctx) | ||
return super().list_commands(ctx) | ||
|
||
def get_command(self, ctx: click.Context, name: str) -> Optional[Command]: | ||
self.maybe_add_run(ctx) | ||
return super().get_command(ctx, name) | ||
|
||
def invoke(self, ctx: click.Context) -> Any: | ||
self.maybe_add_run(ctx) | ||
return super().invoke(ctx) | ||
|
||
def maybe_add_run(self, ctx: click.Context) -> None: | ||
maybe_update_state(ctx) | ||
maybe_add_run_to_cli(self) | ||
|
||
|
||
def get_typer_from_module(module: Any) -> Optional[typer.Typer]: | ||
# Try to get defined app | ||
if state.app: | ||
obj = getattr(module, state.app, None) | ||
if not isinstance(obj, typer.Typer): | ||
typer.echo(f"Not a Typer object: --app {state.app}", err=True) | ||
sys.exit(1) | ||
return obj | ||
# Try to get defined function | ||
if state.func: | ||
func_obj = getattr(module, state.func, None) | ||
if not callable(func_obj): | ||
typer.echo(f"Not a function: --func {state.func}", err=True) | ||
sys.exit(1) | ||
sub_app = typer.Typer() | ||
sub_app.command()(func_obj) | ||
return sub_app | ||
# Iterate and get a default object to use as CLI | ||
local_names = dir(module) | ||
local_names_set = set(local_names) | ||
# Try to get a default Typer app | ||
for name in default_app_names: | ||
if name in local_names_set: | ||
obj = getattr(module, name, None) | ||
if isinstance(obj, typer.Typer): | ||
return obj | ||
# Try to get any Typer app | ||
for name in local_names_set - set(default_app_names): | ||
obj = getattr(module, name) | ||
if isinstance(obj, typer.Typer): | ||
return obj | ||
# Try to get a default function | ||
for func_name in default_func_names: | ||
func_obj = getattr(module, func_name, None) | ||
if callable(func_obj): | ||
sub_app = typer.Typer() | ||
sub_app.command()(func_obj) | ||
return sub_app | ||
# Try to get any func app | ||
for func_name in local_names_set - set(default_func_names): | ||
func_obj = getattr(module, func_name) | ||
if callable(func_obj): | ||
sub_app = typer.Typer() | ||
sub_app.command()(func_obj) | ||
return sub_app | ||
return None | ||
|
||
|
||
def get_typer_from_state() -> Optional[typer.Typer]: | ||
spec = None | ||
if state.file: | ||
module_name = state.file.name | ||
spec = importlib.util.spec_from_file_location(module_name, str(state.file)) | ||
elif state.module: | ||
spec = importlib.util.find_spec(state.module) | ||
if spec is None: | ||
if state.file: | ||
typer.echo(f"Could not import as Python file: {state.file}", err=True) | ||
else: | ||
typer.echo(f"Could not import as Python module: {state.module}", err=True) | ||
sys.exit(1) | ||
module = importlib.util.module_from_spec(spec) | ||
spec.loader.exec_module(module) # type: ignore | ||
obj = get_typer_from_module(module) | ||
return obj | ||
|
||
|
||
def maybe_add_run_to_cli(cli: click.Group) -> None: | ||
if "run" not in cli.commands: | ||
if state.file or state.module: | ||
obj = get_typer_from_state() | ||
if obj: | ||
obj._add_completion = False | ||
click_obj = typer.main.get_command(obj) | ||
click_obj.name = "run" | ||
if not click_obj.help: | ||
click_obj.help = "Run the provided Typer app." | ||
cli.add_command(click_obj) | ||
|
||
|
||
@utils_app.callback(cls=TyperCLIGroup, no_args_is_help=True) | ||
def callback( | ||
ctx: typer.Context, | ||
*, | ||
path_or_module: str = typer.Argument(None), | ||
app: str = typer.Option(None, help="The typer app object/variable to use."), | ||
func: str = typer.Option(None, help="The function to convert to Typer."), | ||
) -> None: | ||
""" | ||
Run Typer scripts with completion, without having to create a package. | ||
You probably want to install completion for the typer command: | ||
$ typer --install-completion | ||
https://typer.tiangolo.com/ | ||
""" | ||
maybe_update_state(ctx) | ||
|
||
|
||
def get_docs_for_click( | ||
*, | ||
obj: Command, | ||
ctx: typer.Context, | ||
indent: int = 0, | ||
name: str = "", | ||
call_prefix: str = "", | ||
title: Optional[str] = None, | ||
) -> str: | ||
docs = "#" * (1 + indent) | ||
command_name = name or obj.name | ||
if call_prefix: | ||
command_name = f"{call_prefix} {command_name}" | ||
if not title: | ||
title = f"`{command_name}`" if command_name else "CLI" | ||
docs += f" {title}\n\n" | ||
if obj.help: | ||
docs += f"{obj.help}\n\n" | ||
usage_pieces = obj.collect_usage_pieces(ctx) | ||
if usage_pieces: | ||
docs += "**Usage**:\n\n" | ||
docs += "```console\n" | ||
docs += "$ " | ||
if command_name: | ||
docs += f"{command_name} " | ||
docs += f"{' '.join(usage_pieces)}\n" | ||
docs += "```\n\n" | ||
args = [] | ||
opts = [] | ||
for param in obj.get_params(ctx): | ||
rv = param.get_help_record(ctx) | ||
if rv is not None: | ||
if param.param_type_name == "argument": | ||
args.append(rv) | ||
elif param.param_type_name == "option": | ||
opts.append(rv) | ||
if args: | ||
docs += "**Arguments**:\n\n" | ||
for arg_name, arg_help in args: | ||
docs += f"* `{arg_name}`" | ||
if arg_help: | ||
docs += f": {arg_help}" | ||
docs += "\n" | ||
docs += "\n" | ||
if opts: | ||
docs += "**Options**:\n\n" | ||
for opt_name, opt_help in opts: | ||
docs += f"* `{opt_name}`" | ||
if opt_help: | ||
docs += f": {opt_help}" | ||
docs += "\n" | ||
docs += "\n" | ||
if obj.epilog: | ||
docs += f"{obj.epilog}\n\n" | ||
if isinstance(obj, Group): | ||
group = obj | ||
commands = group.list_commands(ctx) | ||
if commands: | ||
docs += "**Commands**:\n\n" | ||
for command in commands: | ||
command_obj = group.get_command(ctx, command) | ||
assert command_obj | ||
docs += f"* `{command_obj.name}`" | ||
command_help = command_obj.get_short_help_str(limit=1000) | ||
if command_help: | ||
docs += f": {command_help}" | ||
docs += "\n" | ||
docs += "\n" | ||
for command in commands: | ||
command_obj = group.get_command(ctx, command) | ||
assert command_obj | ||
use_prefix = "" | ||
if command_name: | ||
use_prefix += f"{command_name}" | ||
docs += get_docs_for_click(obj=command_obj, ctx=ctx, indent=indent + 1, call_prefix=use_prefix) | ||
return docs | ||
|
||
|
||
@utils_app.command() | ||
def docs( | ||
ctx: typer.Context, | ||
name: str = typer.Option("", help="The name of the CLI program to use in docs."), | ||
output: Optional[Path] = typer.Option( | ||
None, | ||
help="An output file to write docs to, like README.md.", | ||
file_okay=True, | ||
dir_okay=False, | ||
), | ||
title: Optional[str] = typer.Option( | ||
None, | ||
help="The title for the documentation page. If not provided, the name of " "the program is used.", | ||
), | ||
) -> None: | ||
""" | ||
Generate Markdown docs for a Typer app. | ||
""" | ||
typer_obj = get_typer_from_state() | ||
if not typer_obj: | ||
typer.echo("No Typer app found", err=True) | ||
raise typer.Abort() | ||
click_obj = typer.main.get_command(typer_obj) | ||
docs = get_docs_for_click(obj=click_obj, ctx=ctx, name=name, title=title) | ||
clean_docs = f"{docs.strip()}\n" | ||
if output: | ||
output.write_text(clean_docs) | ||
typer.echo(f"Docs saved to: {output}") | ||
else: | ||
typer.echo(clean_docs) | ||
|
||
|
||
utils_app() |