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