File: //opt/alt/python311/lib/python3.11/site-packages/typer/cli.py
import importlib.util
import re
import sys
from pathlib import Path
from typing import Any, List, Optional
import click
import typer
import typer.core
from click import Command, Group, Option
from . import __version__
default_app_names = ("app", "cli", "main")
default_func_names = ("main", "cli", "app")
app = typer.Typer()
utils_app = typer.Typer(help="Extra utility commands for Typer apps.")
app.add_typer(utils_app, name="utils")
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)
def print_version(ctx: click.Context, param: Option, value: bool) -> None:
if not value or ctx.resilient_parsing:
return
typer.echo(f"Typer version: {__version__}")
raise typer.Exit()
@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."),
version: bool = typer.Option(
False,
"--version",
help="Print version and exit.",
callback=print_version,
),
) -> 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()
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)
def main() -> Any:
return app()