typer-examples 1.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- typer_examples/__init__.py +51 -0
- typer_examples/_hook.py +83 -0
- typer_examples/_models.py +47 -0
- typer_examples/_providers.py +102 -0
- typer_examples/_renderer.py +92 -0
- typer_examples/docs.py +91 -0
- typer_examples-1.1.1.dist-info/METADATA +143 -0
- typer_examples-1.1.1.dist-info/RECORD +10 -0
- typer_examples-1.1.1.dist-info/WHEEL +4 -0
- typer_examples-1.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError
|
|
4
|
+
from importlib.metadata import version as _pkg_version
|
|
5
|
+
from typing import Callable, Optional
|
|
6
|
+
|
|
7
|
+
import typer as _typer
|
|
8
|
+
|
|
9
|
+
from ._hook import install_hook, register_app, set_config
|
|
10
|
+
from ._models import Example, ExamplesConfig
|
|
11
|
+
from .docs import get_all_examples
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"example",
|
|
15
|
+
"install",
|
|
16
|
+
"configure",
|
|
17
|
+
"ExamplesConfig",
|
|
18
|
+
"get_all_examples",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
__version__ = _pkg_version("typer-examples")
|
|
23
|
+
except PackageNotFoundError: # source tree, not installed
|
|
24
|
+
__version__ = "0.0.0.dev0"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def example(
|
|
28
|
+
desc: str,
|
|
29
|
+
code: str,
|
|
30
|
+
detail: str = "",
|
|
31
|
+
**vars: str,
|
|
32
|
+
) -> Callable:
|
|
33
|
+
ex = Example(desc=desc, code=code, detail=detail, vars=vars)
|
|
34
|
+
|
|
35
|
+
def decorator(fn: Callable) -> Callable:
|
|
36
|
+
if not hasattr(fn, "_typer_examples"):
|
|
37
|
+
fn._typer_examples = [] # type: ignore[attr-defined]
|
|
38
|
+
fn._typer_examples.insert(0, ex) # type: ignore[attr-defined]
|
|
39
|
+
return fn
|
|
40
|
+
|
|
41
|
+
return decorator
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def install(app: _typer.Typer, config: Optional[ExamplesConfig] = None) -> None:
|
|
45
|
+
cfg = config or ExamplesConfig()
|
|
46
|
+
register_app(app, cfg)
|
|
47
|
+
install_hook()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def configure(config: ExamplesConfig) -> None:
|
|
51
|
+
set_config(config)
|
typer_examples/_hook.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import weakref
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
import typer.rich_utils as _ut
|
|
10
|
+
|
|
11
|
+
from ._models import ExamplesConfig
|
|
12
|
+
|
|
13
|
+
_MARKER = "_typer_examples_installed"
|
|
14
|
+
|
|
15
|
+
_config: ExamplesConfig = ExamplesConfig()
|
|
16
|
+
|
|
17
|
+
_app_config_map: weakref.WeakKeyDictionary = weakref.WeakKeyDictionary()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_config() -> ExamplesConfig:
|
|
21
|
+
return _config
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def set_config(cfg: ExamplesConfig) -> None:
|
|
25
|
+
global _config
|
|
26
|
+
_config = cfg
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def register_app(app: Any, cfg: ExamplesConfig) -> None:
|
|
30
|
+
_app_config_map[app] = cfg
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_config_for_callback(callback: Any) -> ExamplesConfig:
|
|
34
|
+
original = callback
|
|
35
|
+
while hasattr(original, "__wrapped__"):
|
|
36
|
+
original = original.__wrapped__
|
|
37
|
+
|
|
38
|
+
for app, cfg in list(_app_config_map.items()):
|
|
39
|
+
if _app_directly_has_callback(app, original):
|
|
40
|
+
return cfg
|
|
41
|
+
return _config
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _app_directly_has_callback(app: Any, callback: Any) -> bool:
|
|
45
|
+
for cmd_info in app.registered_commands:
|
|
46
|
+
if cmd_info.callback is callback:
|
|
47
|
+
return True
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def install_hook() -> None:
|
|
52
|
+
try:
|
|
53
|
+
import rich # noqa: F401
|
|
54
|
+
except ImportError:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
if getattr(_ut, _MARKER, False):
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
_upstream = _ut.rich_format_help
|
|
61
|
+
|
|
62
|
+
def _patched(
|
|
63
|
+
*, obj: "click.Command", ctx: "click.Context", markup_mode: str
|
|
64
|
+
) -> None:
|
|
65
|
+
_upstream(obj=obj, ctx=ctx, markup_mode=markup_mode)
|
|
66
|
+
_render_examples(obj, ctx)
|
|
67
|
+
|
|
68
|
+
_ut.rich_format_help = _patched # type: ignore[assignment]
|
|
69
|
+
setattr(_ut, _MARKER, True)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _render_examples(obj: "click.Command", ctx: "click.Context") -> None:
|
|
73
|
+
try:
|
|
74
|
+
from ._renderer import print_examples_panel
|
|
75
|
+
except ImportError:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
examples = getattr(getattr(obj, "callback", None), "_typer_examples", None)
|
|
79
|
+
if not examples:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
config = _get_config_for_callback(obj.callback)
|
|
83
|
+
print_examples_panel(examples, obj, ctx, config)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Data models for typer-examples."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Callable, Dict, Optional, Tuple, Union
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Example:
|
|
11
|
+
"""A single CLI usage example attached to a command.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
desc: Short heading (5-8 words, imperative). Answers: *what is this example for?*
|
|
15
|
+
code: The CLI invocation string. May contain ``{placeholder}`` template variables.
|
|
16
|
+
detail: Optional prose line. Answers: *why or when to use this invocation?*
|
|
17
|
+
vars: Per-example template variable overrides (highest priority in resolution chain).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
desc: str
|
|
21
|
+
code: str
|
|
22
|
+
detail: str = ""
|
|
23
|
+
vars: Dict[str, str] = field(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Template var values can be a plain string or a lazy callable(ctx) -> str
|
|
27
|
+
VarValue = Union[str, Callable[..., str]]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ExamplesConfig:
|
|
32
|
+
"""Global rendering configuration for typer-examples.
|
|
33
|
+
|
|
34
|
+
Style fields that are ``None`` are auto-matched to the values Typer itself uses
|
|
35
|
+
(read from ``typer.rich_utils`` at render time).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
panel_title: str = "Examples"
|
|
39
|
+
panel_border_style: Optional[str] = None
|
|
40
|
+
panel_padding: Optional[Tuple[Any, ...]] = None
|
|
41
|
+
title_align: Optional[str] = None
|
|
42
|
+
heading_style: str = "bold"
|
|
43
|
+
detail_style: str = "dim"
|
|
44
|
+
syntax_theme: str = "ansi_dark"
|
|
45
|
+
syntax_highlight: bool = True
|
|
46
|
+
show_command_prefix: bool = True
|
|
47
|
+
vars: Dict[str, VarValue] = field(default_factory=dict)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
# Click uses a Sentinel singleton (not None) for required args that have no default.
|
|
9
|
+
# We must exclude it from the template-var resolution, otherwise str(Sentinel.UNSET)
|
|
10
|
+
# leaks as a literal "Sentinel.UNSET" placeholder value.
|
|
11
|
+
try:
|
|
12
|
+
from click.core import Sentinel as _ClickSentinel
|
|
13
|
+
|
|
14
|
+
_CLICK_UNSET = _ClickSentinel.UNSET
|
|
15
|
+
except (ImportError, AttributeError):
|
|
16
|
+
# Older Click versions don't have this sentinel; use a private object that
|
|
17
|
+
# will never match any real default value.
|
|
18
|
+
_CLICK_UNSET = object()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _SafeFormatMap(dict):
|
|
22
|
+
"""dict subclass that returns '{key}' for missing keys instead of raising KeyError."""
|
|
23
|
+
|
|
24
|
+
def __missing__(self, key: str) -> str:
|
|
25
|
+
return f"{{{key}}}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolve_vars(
|
|
29
|
+
obj: click.Command,
|
|
30
|
+
ctx: click.Context,
|
|
31
|
+
config_vars: Dict[str, Any],
|
|
32
|
+
) -> Dict[str, str]:
|
|
33
|
+
"""Build the template-variable mapping for a single render pass.
|
|
34
|
+
|
|
35
|
+
Resolution priority (highest → lowest):
|
|
36
|
+
1. Auto-fill from ``sys.argv`` positional args before ``--help`` (merged by
|
|
37
|
+
the renderer after this returns, always wins over per-example kwargs)
|
|
38
|
+
2. Per-example kwargs (merged by the renderer after this returns)
|
|
39
|
+
3. App-level ``configure(vars={...})`` — callables evaluated here
|
|
40
|
+
4. Parameter defaults from the Click command definition
|
|
41
|
+
"""
|
|
42
|
+
result: Dict[str, str] = {}
|
|
43
|
+
|
|
44
|
+
positional_params = [p for p in obj.params if isinstance(p, click.Argument)]
|
|
45
|
+
|
|
46
|
+
for param in positional_params:
|
|
47
|
+
if param.default is not None and param.default is not _CLICK_UNSET:
|
|
48
|
+
result[param.name] = str(param.default) # type: ignore[arg-type]
|
|
49
|
+
|
|
50
|
+
argv_values = _extract_argv_positionals(obj, ctx)
|
|
51
|
+
result.update(argv_values)
|
|
52
|
+
|
|
53
|
+
for key, val in config_vars.items():
|
|
54
|
+
if callable(val):
|
|
55
|
+
try:
|
|
56
|
+
result[key] = str(val(ctx))
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
else:
|
|
60
|
+
result[key] = str(val)
|
|
61
|
+
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _extract_argv_positionals(
|
|
66
|
+
obj: click.Command,
|
|
67
|
+
ctx: click.Context,
|
|
68
|
+
) -> Dict[str, str]:
|
|
69
|
+
positional_params = [p for p in obj.params if isinstance(p, click.Argument)]
|
|
70
|
+
if not positional_params:
|
|
71
|
+
return {}
|
|
72
|
+
|
|
73
|
+
known_subcommands: set[str] = set()
|
|
74
|
+
c: click.Context | None = ctx
|
|
75
|
+
while c is not None:
|
|
76
|
+
if isinstance(c.command, click.Group):
|
|
77
|
+
known_subcommands.update(c.command.list_commands(c))
|
|
78
|
+
c = c.parent
|
|
79
|
+
|
|
80
|
+
raw_args = list(sys.argv[1:])
|
|
81
|
+
try:
|
|
82
|
+
help_idx = next(i for i, a in enumerate(raw_args) if a in ("--help", "-h"))
|
|
83
|
+
raw_args = raw_args[:help_idx]
|
|
84
|
+
except StopIteration:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
command_path_tokens = ctx.command_path.split()
|
|
88
|
+
for token in command_path_tokens:
|
|
89
|
+
if raw_args and raw_args[0] == token:
|
|
90
|
+
raw_args = raw_args[1:]
|
|
91
|
+
|
|
92
|
+
positional_values = [a for a in raw_args if not a.startswith("-") and a not in known_subcommands]
|
|
93
|
+
|
|
94
|
+
result: Dict[str, str] = {}
|
|
95
|
+
for param, value in zip(positional_params, positional_values):
|
|
96
|
+
result[param.name] = value # type: ignore[assignment]
|
|
97
|
+
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def safe_format_map(text: str, vars: Dict[str, str]) -> str:
|
|
102
|
+
return text.format_map(_SafeFormatMap(vars))
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, List
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from rich.console import Group
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.syntax import Syntax
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from ._models import Example, ExamplesConfig
|
|
14
|
+
from ._providers import _extract_argv_positionals, resolve_vars, safe_format_map
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def print_examples_panel(
|
|
18
|
+
examples: List[Example],
|
|
19
|
+
obj: "click.Command",
|
|
20
|
+
ctx: "click.Context",
|
|
21
|
+
config: ExamplesConfig,
|
|
22
|
+
) -> None:
|
|
23
|
+
import typer.rich_utils as ut
|
|
24
|
+
|
|
25
|
+
console = ut._get_rich_console() # type: ignore[attr-defined]
|
|
26
|
+
template_vars = resolve_vars(obj, ctx, config.vars)
|
|
27
|
+
argv_vars = _extract_argv_positionals(obj, ctx)
|
|
28
|
+
|
|
29
|
+
border_style = (
|
|
30
|
+
config.panel_border_style
|
|
31
|
+
if config.panel_border_style is not None
|
|
32
|
+
else getattr(ut, "STYLE_OPTIONS_PANEL_BORDER", "dim")
|
|
33
|
+
)
|
|
34
|
+
padding = (
|
|
35
|
+
config.panel_padding if config.panel_padding is not None else getattr(ut, "STYLE_OPTIONS_TABLE_PADDING", (0, 1))
|
|
36
|
+
)
|
|
37
|
+
title_align = config.title_align if config.title_align is not None else getattr(ut, "ALIGN_OPTIONS_PANEL", "left")
|
|
38
|
+
|
|
39
|
+
rows = []
|
|
40
|
+
|
|
41
|
+
for i, ex in enumerate(examples):
|
|
42
|
+
merged_vars = {**template_vars, **ex.vars, **argv_vars}
|
|
43
|
+
|
|
44
|
+
desc = safe_format_map(ex.desc, merged_vars)
|
|
45
|
+
code = _build_code(ex.code, obj, ctx, merged_vars, config)
|
|
46
|
+
detail = safe_format_map(ex.detail, merged_vars) if ex.detail else None
|
|
47
|
+
|
|
48
|
+
parts: list = [Text(desc, style=config.heading_style)]
|
|
49
|
+
|
|
50
|
+
if detail:
|
|
51
|
+
parts.append(Text(detail, style=config.detail_style))
|
|
52
|
+
|
|
53
|
+
if config.syntax_highlight:
|
|
54
|
+
parts.append(
|
|
55
|
+
Syntax(
|
|
56
|
+
f"$ {code}",
|
|
57
|
+
"bash",
|
|
58
|
+
theme=config.syntax_theme,
|
|
59
|
+
background_color="default",
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
parts.append(Text(f"$ {code}"))
|
|
64
|
+
|
|
65
|
+
rows.append(Group(*parts))
|
|
66
|
+
|
|
67
|
+
if i < len(examples) - 1:
|
|
68
|
+
rows.append(Text(""))
|
|
69
|
+
|
|
70
|
+
console.print(
|
|
71
|
+
Panel(
|
|
72
|
+
Group(*rows),
|
|
73
|
+
padding=padding,
|
|
74
|
+
border_style=border_style,
|
|
75
|
+
title=config.panel_title,
|
|
76
|
+
title_align=title_align,
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _build_code(
|
|
82
|
+
code: str,
|
|
83
|
+
obj: "click.Command",
|
|
84
|
+
ctx: "click.Context",
|
|
85
|
+
vars: dict,
|
|
86
|
+
config: ExamplesConfig,
|
|
87
|
+
) -> str:
|
|
88
|
+
resolved = safe_format_map(code, vars)
|
|
89
|
+
if config.show_command_prefix:
|
|
90
|
+
prefix = ctx.command_path
|
|
91
|
+
resolved = f"{prefix} {resolved}".strip()
|
|
92
|
+
return resolved
|
typer_examples/docs.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Optional, Tuple
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from ._providers import safe_format_map
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_all_examples(app: typer.Typer) -> Dict[Tuple[str, ...], list]:
|
|
11
|
+
from ._models import Example
|
|
12
|
+
|
|
13
|
+
result: Dict[Tuple[str, ...], List[Example]] = {}
|
|
14
|
+
_collect(app, path=(), result=result)
|
|
15
|
+
return result
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _collect(
|
|
19
|
+
app: typer.Typer,
|
|
20
|
+
path: Tuple[str, ...],
|
|
21
|
+
result: Dict[Tuple[str, ...], list],
|
|
22
|
+
) -> None:
|
|
23
|
+
for command_info in app.registered_commands:
|
|
24
|
+
name = command_info.name or (
|
|
25
|
+
command_info.callback.__name__.replace("_", "-")
|
|
26
|
+
if command_info.callback
|
|
27
|
+
else None
|
|
28
|
+
)
|
|
29
|
+
if not name:
|
|
30
|
+
continue
|
|
31
|
+
examples = getattr(command_info.callback, "_typer_examples", None)
|
|
32
|
+
if examples:
|
|
33
|
+
result[path + (name,)] = list(examples)
|
|
34
|
+
|
|
35
|
+
for group_info in app.registered_groups:
|
|
36
|
+
sub_app = group_info.typer_instance
|
|
37
|
+
if sub_app is None:
|
|
38
|
+
continue
|
|
39
|
+
group_name = group_info.name or ""
|
|
40
|
+
_collect(sub_app, path + (group_name,) if group_name else path, result)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def to_markdown(app: typer.Typer, vars: Optional[Dict[str, str]] = None) -> str:
|
|
44
|
+
all_examples = get_all_examples(app)
|
|
45
|
+
lines: List[str] = []
|
|
46
|
+
|
|
47
|
+
for path, examples in all_examples.items():
|
|
48
|
+
command_str = " ".join(path)
|
|
49
|
+
lines.append(f"## `{command_str}`\n")
|
|
50
|
+
for ex in examples:
|
|
51
|
+
lines.append(f"**{ex.desc}**\n")
|
|
52
|
+
if ex.detail:
|
|
53
|
+
lines.append(f"{ex.detail}\n")
|
|
54
|
+
code = (
|
|
55
|
+
safe_format_map(ex.code, {**vars, **ex.vars})
|
|
56
|
+
if vars is not None
|
|
57
|
+
else ex.code
|
|
58
|
+
)
|
|
59
|
+
lines.append(f"```bash\n$ {code}\n```\n")
|
|
60
|
+
|
|
61
|
+
return "\n".join(lines)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def to_rst(app: typer.Typer, vars: Optional[Dict[str, str]] = None) -> str:
|
|
65
|
+
all_examples = get_all_examples(app)
|
|
66
|
+
lines: List[str] = []
|
|
67
|
+
|
|
68
|
+
for path, examples in all_examples.items():
|
|
69
|
+
command_str = " ".join(path)
|
|
70
|
+
heading = f"``{command_str}``"
|
|
71
|
+
lines.append(heading)
|
|
72
|
+
lines.append("~" * len(heading))
|
|
73
|
+
lines.append("")
|
|
74
|
+
|
|
75
|
+
for ex in examples:
|
|
76
|
+
lines.append(f"**{ex.desc}**")
|
|
77
|
+
lines.append("")
|
|
78
|
+
if ex.detail:
|
|
79
|
+
lines.append(ex.detail)
|
|
80
|
+
lines.append("")
|
|
81
|
+
lines.append(".. code-block:: bash")
|
|
82
|
+
lines.append("")
|
|
83
|
+
code = (
|
|
84
|
+
safe_format_map(ex.code, {**vars, **ex.vars})
|
|
85
|
+
if vars is not None
|
|
86
|
+
else ex.code
|
|
87
|
+
)
|
|
88
|
+
lines.append(f" $ {code}")
|
|
89
|
+
lines.append("")
|
|
90
|
+
|
|
91
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: typer-examples
|
|
3
|
+
Version: 1.1.1
|
|
4
|
+
Summary: Beautiful example rendering for Typer CLI help output
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: cli,examples,help,rich,typer
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: typer>=0.9.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: git-cliff>=2.6; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: typer[all]>=0.9.0; extra == 'dev'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# typer-examples
|
|
27
|
+
|
|
28
|
+
[](https://github.com/Xieyt/typer-examples/actions/workflows/ci.yml)
|
|
29
|
+
[](https://github.com/Xieyt/typer-examples/actions/workflows/typer-compat.yml)
|
|
30
|
+
[](#compatibility)
|
|
31
|
+
[](#license)
|
|
32
|
+
|
|
33
|
+
Attach structured, syntax-highlighted usage examples to [Typer](https://typer.tiangolo.com/) commands. They appear automatically in `--help` output as a Rich panel — no subclassing, no epilog hacks.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Usage: myapp deploy [OPTIONS] ENV
|
|
37
|
+
|
|
38
|
+
╭─ Options ──────────────────────────────────────────────────────────╮
|
|
39
|
+
│ --tag TEXT Docker image tag to deploy. [default: latest] │
|
|
40
|
+
│ --yes Skip confirmation prompt. │
|
|
41
|
+
│ --help Show this message and exit. │
|
|
42
|
+
╰────────────────────────────────────────────────────────────────────╯
|
|
43
|
+
╭─ Examples ─────────────────────────────────────────────────────────╮
|
|
44
|
+
│ Deploy to staging with a pinned tag │
|
|
45
|
+
│ Pulls the image, runs migrations, and restarts services. │
|
|
46
|
+
│ $ myapp deploy staging --tag 1.4.2 │
|
|
47
|
+
│ │
|
|
48
|
+
│ Deploy to production and skip confirmation │
|
|
49
|
+
│ Pass --yes in CI pipelines to avoid interactive prompts. │
|
|
50
|
+
│ $ myapp deploy production --tag 2.0.0 --yes │
|
|
51
|
+
╰────────────────────────────────────────────────────────────────────╯
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install typer-examples
|
|
58
|
+
# or
|
|
59
|
+
uv add typer-examples
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Requires Python ≥ 3.9 and Typer ≥ 0.9.
|
|
63
|
+
|
|
64
|
+
## Quick start
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
import typer
|
|
68
|
+
from typer_examples import example, install
|
|
69
|
+
|
|
70
|
+
app = typer.Typer()
|
|
71
|
+
install(app)
|
|
72
|
+
|
|
73
|
+
@app.command()
|
|
74
|
+
@example(
|
|
75
|
+
"Deploy to staging with a pinned tag",
|
|
76
|
+
"{env} --tag {version}",
|
|
77
|
+
env="staging",
|
|
78
|
+
version="1.4.2",
|
|
79
|
+
detail="Pulls the image, runs migrations, and restarts services.",
|
|
80
|
+
)
|
|
81
|
+
@example(
|
|
82
|
+
"Deploy to production and skip confirmation",
|
|
83
|
+
"{env} --tag {version} --yes",
|
|
84
|
+
env="production",
|
|
85
|
+
version="2.0.0",
|
|
86
|
+
detail="Pass --yes in CI pipelines to avoid interactive prompts.",
|
|
87
|
+
)
|
|
88
|
+
def deploy(env: str, tag: str = typer.Option("latest"), yes: bool = False):
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
app()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> **Decorator order**: `@example` must sit *below* `@app.command()` — closest to `def`. This ensures the metadata is attached to the raw function before Typer wraps it.
|
|
96
|
+
|
|
97
|
+
## Features
|
|
98
|
+
|
|
99
|
+
- **Decorator-first** — examples live next to the command, not buried in docstrings or epilog strings
|
|
100
|
+
- **Template variables** — `{placeholders}` resolved from per-example kwargs, app-level defaults, `sys.argv`, or Click parameter defaults → [details](docs/template-variables.md)
|
|
101
|
+
- **Per-app config** — each `Typer()` instance gets its own panel title, style theme, and variable defaults → [details](docs/configuration.md)
|
|
102
|
+
- **Docs generation** — export all examples to Markdown or reStructuredText for CI doc pipelines → [API](docs/api.md)
|
|
103
|
+
- **Chain-safe** — composes with `rich-click` and any other `rich_format_help` patch without clobbering
|
|
104
|
+
- **Graceful degradation** — if `rich` is absent, `install()` returns silently with no error
|
|
105
|
+
|
|
106
|
+
## Documentation
|
|
107
|
+
|
|
108
|
+
| Topic | |
|
|
109
|
+
|-------|--|
|
|
110
|
+
| [Template variables](docs/template-variables.md) | Resolution chain: per-example → app-level → argv → defaults |
|
|
111
|
+
| [Configuration](docs/configuration.md) | `ExamplesConfig` fields, global config, per-app config for sub-commands |
|
|
112
|
+
| [API reference](docs/api.md) | Full public API with signatures and return types |
|
|
113
|
+
| [How it works](docs/how-it-works.md) | Internal implementation and the `rich_format_help` hook |
|
|
114
|
+
|
|
115
|
+
## Try the examples
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Minimal single-app setup — three commands, each with examples
|
|
119
|
+
uv run python examples/simple.py deploy --help
|
|
120
|
+
uv run python examples/simple.py logs --help
|
|
121
|
+
uv run python examples/simple.py shell --help
|
|
122
|
+
|
|
123
|
+
# Root app + two sub-apps with independent per-app config
|
|
124
|
+
uv run python examples/app.py deploy --help
|
|
125
|
+
uv run python examples/app.py db migrate --help
|
|
126
|
+
uv run python examples/app.py server start --help
|
|
127
|
+
|
|
128
|
+
# Static docs generation
|
|
129
|
+
uv run python examples/app.py docs
|
|
130
|
+
uv run python examples/app.py docs --format rst
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Compatibility
|
|
134
|
+
|
|
135
|
+
| Typer | Python | rich-click |
|
|
136
|
+
|-------|--------|------------|
|
|
137
|
+
| ≥ 0.9 | 3.9 – 3.12 | Supported |
|
|
138
|
+
|
|
139
|
+
Compatibility is verified daily across all supported Typer × Python combinations in CI.
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
typer_examples/__init__.py,sha256=k2Brsn-ASPNxABZg2jQNwcbsrSLGNUjCuN1-_6-Y_so,1269
|
|
2
|
+
typer_examples/_hook.py,sha256=lsU1Vafm4K0EzpZwGiSjjBVF4y6y0CkOKNdtXvcmXXU,1999
|
|
3
|
+
typer_examples/_models.py,sha256=ZRCng0TGlAZQvGDs2GCsXt-QVG-_WA04Kok51iPamw4,1516
|
|
4
|
+
typer_examples/_providers.py,sha256=fc2WVhzYFzJKgNdDFlFt6pml0ZjLEngkfXR-tehA1wY,3287
|
|
5
|
+
typer_examples/_renderer.py,sha256=OChScZ0UlaWY_d4X0oRVNxZPpxlRxIcwnGqW8mHV-Dg,2599
|
|
6
|
+
typer_examples/docs.py,sha256=OJvea4Kvtx_KJ40GRStosVyASJmUTOzT3C5w_e1NX2E,2723
|
|
7
|
+
typer_examples-1.1.1.dist-info/METADATA,sha256=1P8EVG4AkYs_0S4hFb9UnnFymFZzcYEFt2ST2kY55Eg,6212
|
|
8
|
+
typer_examples-1.1.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
9
|
+
typer_examples-1.1.1.dist-info/licenses/LICENSE,sha256=3gIpJJaVDsJdEq5q2GxKWznSwv1N2Uu2aHkw-17QJfs,1066
|
|
10
|
+
typer_examples-1.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aloksingh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|