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
app: Optional[str]
func: Optional[str]
file: Optional[pathlib.Path]
module: Optional[str]
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
class TyperCLIGroup(typer.core.TyperGroup):
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 Command objects. Can also be a list of Command, which will use Command.name to create the dict.
  • attrs: Other command arguments described in MultiCommand, Command, and BaseCommand.

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.

def maybe_add_run(self, ctx: click.core.Context) -> None:
68    def maybe_add_run(self, ctx: click.Context) -> None:
69        maybe_update_state(ctx)
70        maybe_add_run_to_cli(self)
Inherited Members
typer.core.TyperGroup
TyperGroup
rich_markup_mode
rich_help_panel
format_options
main
format_help
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
hidden
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
def get_typer_from_module(module: Any) -> Optional[typer.main.Typer]:
 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
def get_typer_from_state() -> Optional[typer.main.Typer]:
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)
@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

https://typer.tiangolo.com/

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:
299def main() -> Any:
300    return app()