typer.cli
1import importlib.util 2import re 3import sys 4from pathlib import Path 5from typing import Any, List, Optional 6 7import click 8import typer 9import typer.core 10from click import Command, Group, Option 11 12from . import __version__ 13 14try: 15 import rich 16 17 has_rich = True 18 from . import rich_utils 19 20except ImportError: # pragma: no cover 21 has_rich = False 22 rich = None # type: ignore 23 24default_app_names = ("app", "cli", "main") 25default_func_names = ("main", "cli", "app") 26 27app = typer.Typer() 28utils_app = typer.Typer(help="Extra utility commands for Typer apps.") 29app.add_typer(utils_app, name="utils") 30 31 32class State: 33 def __init__(self) -> None: 34 self.app: Optional[str] = None 35 self.func: Optional[str] = None 36 self.file: Optional[Path] = None 37 self.module: Optional[str] = None 38 39 40state = State() 41 42 43def maybe_update_state(ctx: click.Context) -> None: 44 path_or_module = ctx.params.get("path_or_module") 45 if path_or_module: 46 file_path = Path(path_or_module) 47 if file_path.exists() and file_path.is_file(): 48 state.file = file_path 49 else: 50 if not re.fullmatch(r"[a-zA-Z_]\w*(\.[a-zA-Z_]\w*)*", path_or_module): 51 typer.echo( 52 f"Not a valid file or Python module: {path_or_module}", err=True 53 ) 54 sys.exit(1) 55 state.module = path_or_module 56 app_name = ctx.params.get("app") 57 if app_name: 58 state.app = app_name 59 func_name = ctx.params.get("func") 60 if func_name: 61 state.func = func_name 62 63 64class TyperCLIGroup(typer.core.TyperGroup): 65 def list_commands(self, ctx: click.Context) -> List[str]: 66 self.maybe_add_run(ctx) 67 return super().list_commands(ctx) 68 69 def get_command(self, ctx: click.Context, name: str) -> Optional[Command]: 70 self.maybe_add_run(ctx) 71 return super().get_command(ctx, name) 72 73 def invoke(self, ctx: click.Context) -> Any: 74 self.maybe_add_run(ctx) 75 return super().invoke(ctx) 76 77 def maybe_add_run(self, ctx: click.Context) -> None: 78 maybe_update_state(ctx) 79 maybe_add_run_to_cli(self) 80 81 82def get_typer_from_module(module: Any) -> Optional[typer.Typer]: 83 # Try to get defined app 84 if state.app: 85 obj = getattr(module, state.app, None) 86 if not isinstance(obj, typer.Typer): 87 typer.echo(f"Not a Typer object: --app {state.app}", err=True) 88 sys.exit(1) 89 return obj 90 # Try to get defined function 91 if state.func: 92 func_obj = getattr(module, state.func, None) 93 if not callable(func_obj): 94 typer.echo(f"Not a function: --func {state.func}", err=True) 95 sys.exit(1) 96 sub_app = typer.Typer() 97 sub_app.command()(func_obj) 98 return sub_app 99 # Iterate and get a default object to use as CLI 100 local_names = dir(module) 101 local_names_set = set(local_names) 102 # Try to get a default Typer app 103 for name in default_app_names: 104 if name in local_names_set: 105 obj = getattr(module, name, None) 106 if isinstance(obj, typer.Typer): 107 return obj 108 # Try to get any Typer app 109 for name in local_names_set - set(default_app_names): 110 obj = getattr(module, name) 111 if isinstance(obj, typer.Typer): 112 return obj 113 # Try to get a default function 114 for func_name in default_func_names: 115 func_obj = getattr(module, func_name, None) 116 if callable(func_obj): 117 sub_app = typer.Typer() 118 sub_app.command()(func_obj) 119 return sub_app 120 # Try to get any func app 121 for func_name in local_names_set - set(default_func_names): 122 func_obj = getattr(module, func_name) 123 if callable(func_obj): 124 sub_app = typer.Typer() 125 sub_app.command()(func_obj) 126 return sub_app 127 return None 128 129 130def get_typer_from_state() -> Optional[typer.Typer]: 131 spec = None 132 if state.file: 133 module_name = state.file.name 134 spec = importlib.util.spec_from_file_location(module_name, str(state.file)) 135 elif state.module: 136 spec = importlib.util.find_spec(state.module) 137 if spec is None: 138 if state.file: 139 typer.echo(f"Could not import as Python file: {state.file}", err=True) 140 else: 141 typer.echo(f"Could not import as Python module: {state.module}", err=True) 142 sys.exit(1) 143 module = importlib.util.module_from_spec(spec) 144 spec.loader.exec_module(module) # type: ignore 145 obj = get_typer_from_module(module) 146 return obj 147 148 149def maybe_add_run_to_cli(cli: click.Group) -> None: 150 if "run" not in cli.commands: 151 if state.file or state.module: 152 obj = get_typer_from_state() 153 if obj: 154 obj._add_completion = False 155 click_obj = typer.main.get_command(obj) 156 click_obj.name = "run" 157 if not click_obj.help: 158 click_obj.help = "Run the provided Typer app." 159 cli.add_command(click_obj) 160 161 162def print_version(ctx: click.Context, param: Option, value: bool) -> None: 163 if not value or ctx.resilient_parsing: 164 return 165 typer.echo(f"Typer version: {__version__}") 166 raise typer.Exit() 167 168 169@app.callback(cls=TyperCLIGroup, no_args_is_help=True) 170def callback( 171 ctx: typer.Context, 172 *, 173 path_or_module: str = typer.Argument(None), 174 app: str = typer.Option(None, help="The typer app object/variable to use."), 175 func: str = typer.Option(None, help="The function to convert to Typer."), 176 version: bool = typer.Option( 177 False, 178 "--version", 179 help="Print version and exit.", 180 callback=print_version, 181 ), 182) -> None: 183 """ 184 Run Typer scripts with completion, without having to create a package. 185 186 You probably want to install completion for the typer command: 187 188 $ typer --install-completion 189 190 https://typer.tiangolo.com/ 191 """ 192 maybe_update_state(ctx) 193 194 195def get_docs_for_click( 196 *, 197 obj: Command, 198 ctx: typer.Context, 199 indent: int = 0, 200 name: str = "", 201 call_prefix: str = "", 202 title: Optional[str] = None, 203) -> str: 204 docs = "#" * (1 + indent) 205 command_name = name or obj.name 206 if call_prefix: 207 command_name = f"{call_prefix} {command_name}" 208 if not title: 209 title = f"`{command_name}`" if command_name else "CLI" 210 docs += f" {title}\n\n" 211 if obj.help: 212 docs += f"{_parse_html(obj.help)}\n\n" 213 usage_pieces = obj.collect_usage_pieces(ctx) 214 if usage_pieces: 215 docs += "**Usage**:\n\n" 216 docs += "```console\n" 217 docs += "$ " 218 if command_name: 219 docs += f"{command_name} " 220 docs += f"{' '.join(usage_pieces)}\n" 221 docs += "```\n\n" 222 args = [] 223 opts = [] 224 for param in obj.get_params(ctx): 225 rv = param.get_help_record(ctx) 226 if rv is not None: 227 if param.param_type_name == "argument": 228 args.append(rv) 229 elif param.param_type_name == "option": 230 opts.append(rv) 231 if args: 232 docs += "**Arguments**:\n\n" 233 for arg_name, arg_help in args: 234 docs += f"* `{arg_name}`" 235 if arg_help: 236 docs += f": {_parse_html(arg_help)}" 237 docs += "\n" 238 docs += "\n" 239 if opts: 240 docs += "**Options**:\n\n" 241 for opt_name, opt_help in opts: 242 docs += f"* `{opt_name}`" 243 if opt_help: 244 docs += f": {_parse_html(opt_help)}" 245 docs += "\n" 246 docs += "\n" 247 if obj.epilog: 248 docs += f"{obj.epilog}\n\n" 249 if isinstance(obj, Group): 250 group = obj 251 commands = group.list_commands(ctx) 252 if commands: 253 docs += "**Commands**:\n\n" 254 for command in commands: 255 command_obj = group.get_command(ctx, command) 256 assert command_obj 257 docs += f"* `{command_obj.name}`" 258 command_help = command_obj.get_short_help_str() 259 if command_help: 260 docs += f": {_parse_html(command_help)}" 261 docs += "\n" 262 docs += "\n" 263 for command in commands: 264 command_obj = group.get_command(ctx, command) 265 assert command_obj 266 use_prefix = "" 267 if command_name: 268 use_prefix += f"{command_name}" 269 docs += get_docs_for_click( 270 obj=command_obj, ctx=ctx, indent=indent + 1, call_prefix=use_prefix 271 ) 272 return docs 273 274 275def _parse_html(input_text: str) -> str: 276 if not has_rich: # pragma: no cover 277 return input_text 278 return rich_utils.rich_to_html(input_text) 279 280 281@utils_app.command() 282def docs( 283 ctx: typer.Context, 284 name: str = typer.Option("", help="The name of the CLI program to use in docs."), 285 output: Optional[Path] = typer.Option( 286 None, 287 help="An output file to write docs to, like README.md.", 288 file_okay=True, 289 dir_okay=False, 290 ), 291 title: Optional[str] = typer.Option( 292 None, 293 help="The title for the documentation page. If not provided, the name of " 294 "the program is used.", 295 ), 296) -> None: 297 """ 298 Generate Markdown docs for a Typer app. 299 """ 300 typer_obj = get_typer_from_state() 301 if not typer_obj: 302 typer.echo("No Typer app found", err=True) 303 raise typer.Abort() 304 click_obj = typer.main.get_command(typer_obj) 305 docs = get_docs_for_click(obj=click_obj, ctx=ctx, name=name, title=title) 306 clean_docs = f"{docs.strip()}\n" 307 if output: 308 output.write_text(clean_docs) 309 typer.echo(f"Docs saved to: {output}") 310 else: 311 typer.echo(clean_docs) 312 313 314def main() -> Any: 315 return app()
default_app_names =
('app', 'cli', 'main')
default_func_names =
('main', 'cli', 'app')
app =
<typer.main.Typer object>
utils_app =
<typer.main.Typer object>
class
State:
33class State: 34 def __init__(self) -> None: 35 self.app: Optional[str] = None 36 self.func: Optional[str] = None 37 self.file: Optional[Path] = None 38 self.module: Optional[str] = None
file: Optional[pathlib.Path]
state =
<State object>
def
maybe_update_state(ctx: click.core.Context) -> None:
44def maybe_update_state(ctx: click.Context) -> None: 45 path_or_module = ctx.params.get("path_or_module") 46 if path_or_module: 47 file_path = Path(path_or_module) 48 if file_path.exists() and file_path.is_file(): 49 state.file = file_path 50 else: 51 if not re.fullmatch(r"[a-zA-Z_]\w*(\.[a-zA-Z_]\w*)*", path_or_module): 52 typer.echo( 53 f"Not a valid file or Python module: {path_or_module}", err=True 54 ) 55 sys.exit(1) 56 state.module = path_or_module 57 app_name = ctx.params.get("app") 58 if app_name: 59 state.app = app_name 60 func_name = ctx.params.get("func") 61 if func_name: 62 state.func = func_name
65class TyperCLIGroup(typer.core.TyperGroup): 66 def list_commands(self, ctx: click.Context) -> List[str]: 67 self.maybe_add_run(ctx) 68 return super().list_commands(ctx) 69 70 def get_command(self, ctx: click.Context, name: str) -> Optional[Command]: 71 self.maybe_add_run(ctx) 72 return super().get_command(ctx, name) 73 74 def invoke(self, ctx: click.Context) -> Any: 75 self.maybe_add_run(ctx) 76 return super().invoke(ctx) 77 78 def maybe_add_run(self, ctx: click.Context) -> None: 79 maybe_update_state(ctx) 80 maybe_add_run_to_cli(self)
A group allows a command to have subcommands attached. This is the most common way to implement nesting in Click.
Parameters
- name: The name of the group command.
- commands: A dict mapping names to
Command
objects. Can also be a list ofCommand
, which will useCommand.name
to create the dict. - attrs: Other command arguments described in
MultiCommand
,Command
, andBaseCommand
.
Changed in version 8.0:
The commands
argument can be a list of command objects.
def
list_commands(self, ctx: click.core.Context) -> List[str]:
66 def list_commands(self, ctx: click.Context) -> List[str]: 67 self.maybe_add_run(ctx) 68 return super().list_commands(ctx)
Returns a list of subcommand names. Note that in Click's Group class, these are sorted. In Typer, we wish to maintain the original order of creation (cf Issue #933)
def
get_command(self, ctx: click.core.Context, name: str) -> Optional[click.core.Command]:
70 def get_command(self, ctx: click.Context, name: str) -> Optional[Command]: 71 self.maybe_add_run(ctx) 72 return super().get_command(ctx, name)
Given a context and a command name, this returns a
Command
object if it exists or returns None
.
def
invoke(self, ctx: click.core.Context) -> Any:
74 def invoke(self, ctx: click.Context) -> Any: 75 self.maybe_add_run(ctx) 76 return super().invoke(ctx)
Given a context, this invokes the attached callback (if it exists) in the right way.
Inherited Members
- click.core.Group
- command_class
- group_class
- commands
- add_command
- command
- group
- click.core.MultiCommand
- allow_extra_args
- allow_interspersed_args
- no_args_is_help
- invoke_without_command
- subcommand_metavar
- chain
- to_info_dict
- collect_usage_pieces
- result_callback
- format_commands
- parse_args
- resolve_command
- shell_complete
- click.core.Command
- callback
- params
- help
- epilog
- options_metavar
- short_help
- add_help_option
- deprecated
- get_usage
- get_params
- format_usage
- get_help_option_names
- get_help_option
- make_parser
- get_help
- get_short_help_str
- format_help_text
- format_epilog
- click.core.BaseCommand
- context_class
- ignore_unknown_options
- name
- context_settings
- make_context
83def get_typer_from_module(module: Any) -> Optional[typer.Typer]: 84 # Try to get defined app 85 if state.app: 86 obj = getattr(module, state.app, None) 87 if not isinstance(obj, typer.Typer): 88 typer.echo(f"Not a Typer object: --app {state.app}", err=True) 89 sys.exit(1) 90 return obj 91 # Try to get defined function 92 if state.func: 93 func_obj = getattr(module, state.func, None) 94 if not callable(func_obj): 95 typer.echo(f"Not a function: --func {state.func}", err=True) 96 sys.exit(1) 97 sub_app = typer.Typer() 98 sub_app.command()(func_obj) 99 return sub_app 100 # Iterate and get a default object to use as CLI 101 local_names = dir(module) 102 local_names_set = set(local_names) 103 # Try to get a default Typer app 104 for name in default_app_names: 105 if name in local_names_set: 106 obj = getattr(module, name, None) 107 if isinstance(obj, typer.Typer): 108 return obj 109 # Try to get any Typer app 110 for name in local_names_set - set(default_app_names): 111 obj = getattr(module, name) 112 if isinstance(obj, typer.Typer): 113 return obj 114 # Try to get a default function 115 for func_name in default_func_names: 116 func_obj = getattr(module, func_name, None) 117 if callable(func_obj): 118 sub_app = typer.Typer() 119 sub_app.command()(func_obj) 120 return sub_app 121 # Try to get any func app 122 for func_name in local_names_set - set(default_func_names): 123 func_obj = getattr(module, func_name) 124 if callable(func_obj): 125 sub_app = typer.Typer() 126 sub_app.command()(func_obj) 127 return sub_app 128 return None
131def get_typer_from_state() -> Optional[typer.Typer]: 132 spec = None 133 if state.file: 134 module_name = state.file.name 135 spec = importlib.util.spec_from_file_location(module_name, str(state.file)) 136 elif state.module: 137 spec = importlib.util.find_spec(state.module) 138 if spec is None: 139 if state.file: 140 typer.echo(f"Could not import as Python file: {state.file}", err=True) 141 else: 142 typer.echo(f"Could not import as Python module: {state.module}", err=True) 143 sys.exit(1) 144 module = importlib.util.module_from_spec(spec) 145 spec.loader.exec_module(module) # type: ignore 146 obj = get_typer_from_module(module) 147 return obj
def
maybe_add_run_to_cli(cli: click.core.Group) -> None:
150def maybe_add_run_to_cli(cli: click.Group) -> None: 151 if "run" not in cli.commands: 152 if state.file or state.module: 153 obj = get_typer_from_state() 154 if obj: 155 obj._add_completion = False 156 click_obj = typer.main.get_command(obj) 157 click_obj.name = "run" 158 if not click_obj.help: 159 click_obj.help = "Run the provided Typer app." 160 cli.add_command(click_obj)
def
print_version(ctx: click.core.Context, param: click.core.Option, value: bool) -> None:
@app.callback(cls=TyperCLIGroup, no_args_is_help=True)
def
callback( ctx: typer.models.Context, *, path_or_module: str = <typer.models.ArgumentInfo object>, app: str = <typer.models.OptionInfo object>, func: str = <typer.models.OptionInfo object>, version: bool = <typer.models.OptionInfo object>) -> None:
170@app.callback(cls=TyperCLIGroup, no_args_is_help=True) 171def callback( 172 ctx: typer.Context, 173 *, 174 path_or_module: str = typer.Argument(None), 175 app: str = typer.Option(None, help="The typer app object/variable to use."), 176 func: str = typer.Option(None, help="The function to convert to Typer."), 177 version: bool = typer.Option( 178 False, 179 "--version", 180 help="Print version and exit.", 181 callback=print_version, 182 ), 183) -> None: 184 """ 185 Run Typer scripts with completion, without having to create a package. 186 187 You probably want to install completion for the typer command: 188 189 $ typer --install-completion 190 191 https://typer.tiangolo.com/ 192 """ 193 maybe_update_state(ctx)
Run Typer scripts with completion, without having to create a package.
You probably want to install completion for the typer command:
$ typer --install-completion
def
get_docs_for_click( *, obj: click.core.Command, ctx: typer.models.Context, indent: int = 0, name: str = '', call_prefix: str = '', title: Optional[str] = None) -> str:
196def get_docs_for_click( 197 *, 198 obj: Command, 199 ctx: typer.Context, 200 indent: int = 0, 201 name: str = "", 202 call_prefix: str = "", 203 title: Optional[str] = None, 204) -> str: 205 docs = "#" * (1 + indent) 206 command_name = name or obj.name 207 if call_prefix: 208 command_name = f"{call_prefix} {command_name}" 209 if not title: 210 title = f"`{command_name}`" if command_name else "CLI" 211 docs += f" {title}\n\n" 212 if obj.help: 213 docs += f"{_parse_html(obj.help)}\n\n" 214 usage_pieces = obj.collect_usage_pieces(ctx) 215 if usage_pieces: 216 docs += "**Usage**:\n\n" 217 docs += "```console\n" 218 docs += "$ " 219 if command_name: 220 docs += f"{command_name} " 221 docs += f"{' '.join(usage_pieces)}\n" 222 docs += "```\n\n" 223 args = [] 224 opts = [] 225 for param in obj.get_params(ctx): 226 rv = param.get_help_record(ctx) 227 if rv is not None: 228 if param.param_type_name == "argument": 229 args.append(rv) 230 elif param.param_type_name == "option": 231 opts.append(rv) 232 if args: 233 docs += "**Arguments**:\n\n" 234 for arg_name, arg_help in args: 235 docs += f"* `{arg_name}`" 236 if arg_help: 237 docs += f": {_parse_html(arg_help)}" 238 docs += "\n" 239 docs += "\n" 240 if opts: 241 docs += "**Options**:\n\n" 242 for opt_name, opt_help in opts: 243 docs += f"* `{opt_name}`" 244 if opt_help: 245 docs += f": {_parse_html(opt_help)}" 246 docs += "\n" 247 docs += "\n" 248 if obj.epilog: 249 docs += f"{obj.epilog}\n\n" 250 if isinstance(obj, Group): 251 group = obj 252 commands = group.list_commands(ctx) 253 if commands: 254 docs += "**Commands**:\n\n" 255 for command in commands: 256 command_obj = group.get_command(ctx, command) 257 assert command_obj 258 docs += f"* `{command_obj.name}`" 259 command_help = command_obj.get_short_help_str() 260 if command_help: 261 docs += f": {_parse_html(command_help)}" 262 docs += "\n" 263 docs += "\n" 264 for command in commands: 265 command_obj = group.get_command(ctx, command) 266 assert command_obj 267 use_prefix = "" 268 if command_name: 269 use_prefix += f"{command_name}" 270 docs += get_docs_for_click( 271 obj=command_obj, ctx=ctx, indent=indent + 1, call_prefix=use_prefix 272 ) 273 return docs
@utils_app.command()
def
docs( ctx: typer.models.Context, name: str = <typer.models.OptionInfo object>, output: Optional[pathlib.Path] = <typer.models.OptionInfo object>, title: Optional[str] = <typer.models.OptionInfo object>) -> None:
282@utils_app.command() 283def docs( 284 ctx: typer.Context, 285 name: str = typer.Option("", help="The name of the CLI program to use in docs."), 286 output: Optional[Path] = typer.Option( 287 None, 288 help="An output file to write docs to, like README.md.", 289 file_okay=True, 290 dir_okay=False, 291 ), 292 title: Optional[str] = typer.Option( 293 None, 294 help="The title for the documentation page. If not provided, the name of " 295 "the program is used.", 296 ), 297) -> None: 298 """ 299 Generate Markdown docs for a Typer app. 300 """ 301 typer_obj = get_typer_from_state() 302 if not typer_obj: 303 typer.echo("No Typer app found", err=True) 304 raise typer.Abort() 305 click_obj = typer.main.get_command(typer_obj) 306 docs = get_docs_for_click(obj=click_obj, ctx=ctx, name=name, title=title) 307 clean_docs = f"{docs.strip()}\n" 308 if output: 309 output.write_text(clean_docs) 310 typer.echo(f"Docs saved to: {output}") 311 else: 312 typer.echo(clean_docs)
Generate Markdown docs for a Typer app.
def
main() -> Any: