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
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:
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
class TyperCLIGroup(typer.core.TyperGroup):
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 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]:
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.

def maybe_add_run(self, ctx: click.core.Context) -> None:
78    def maybe_add_run(self, ctx: click.Context) -> None:
79        maybe_update_state(ctx)
80        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]:
 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
def get_typer_from_state() -> Optional[typer.main.Typer]:
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)
@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

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