Skip to content

Commit 52e03b9

Browse files
authored
Add link to subcommands (#55)
* Subcommands are now listed as part of the command * Document new ability in README.md * Fix style errors * `list_subcommands` is no longer True by default * Added more unit tests for `list_commands` * Updated README.md
1 parent 4349253 commit 52e03b9

File tree

8 files changed

+340
-22
lines changed

8 files changed

+340
-22
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,5 @@ Options:
136136
- `style`: _(Optional, default: `plain`)_ Style for the options section. The possible choices are `plain` and `table`.
137137
- `remove_ascii_art`: _(Optional, default: `False`)_ When docstrings begin with the escape character `\b`, all text will be ignored until the next blank line is encountered.
138138
- `show_hidden`: _(Optional, default: `False`)_ Show commands and options that are marked as hidden.
139+
- `list_subcommands`: _(Optional, default: `False`)_ List subcommands of a given command. If _attr_list_ is installed,
140+
add links to subcommands also.

mkdocs_click/_docs.py

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Licensed under the Apache license (see LICENSE)
44
import inspect
55
from contextlib import contextmanager, ExitStack
6-
from typing import Iterator, List, cast
6+
from typing import Iterator, List, cast, Optional
77

88
import click
99
from markdown.extensions.toc import slugify
@@ -18,6 +18,7 @@ def make_command_docs(
1818
style: str = "plain",
1919
remove_ascii_art: bool = False,
2020
show_hidden: bool = False,
21+
list_subcommands: bool = False,
2122
has_attr_list: bool = False,
2223
) -> Iterator[str]:
2324
"""Create the Markdown lines for a command and its sub-commands."""
@@ -28,6 +29,7 @@ def make_command_docs(
2829
style=style,
2930
remove_ascii_art=remove_ascii_art,
3031
show_hidden=show_hidden,
32+
list_subcommands=list_subcommands,
3133
has_attr_list=has_attr_list,
3234
):
3335
if line.strip() == "\b":
@@ -44,10 +46,11 @@ def _recursively_make_command_docs(
4446
style: str = "plain",
4547
remove_ascii_art: bool = False,
4648
show_hidden: bool = False,
49+
list_subcommands: bool = False,
4750
has_attr_list: bool = False,
4851
) -> Iterator[str]:
4952
"""Create the raw Markdown lines for a command and its sub-commands."""
50-
ctx = click.Context(cast(click.Command, command), info_name=prog_name, parent=parent)
53+
ctx = _build_command_context(prog_name=prog_name, command=command, parent=parent)
5154

5255
if ctx.command.hidden and not show_hidden:
5356
return
@@ -58,24 +61,43 @@ def _recursively_make_command_docs(
5861
yield from _make_options(ctx, style, show_hidden=show_hidden)
5962

6063
subcommands = _get_sub_commands(ctx.command, ctx)
64+
if len(subcommands) == 0:
65+
return
66+
67+
subcommands.sort(key=lambda cmd: str(cmd.name))
68+
69+
if list_subcommands:
70+
yield from _make_subcommands_links(
71+
subcommands,
72+
ctx,
73+
has_attr_list=has_attr_list,
74+
show_hidden=show_hidden,
75+
)
6176

62-
for command in sorted(subcommands, key=lambda cmd: cmd.name): # type: ignore
77+
for command in subcommands:
6378
yield from _recursively_make_command_docs(
6479
cast(str, command.name),
6580
command,
6681
parent=ctx,
6782
depth=depth + 1,
6883
style=style,
6984
show_hidden=show_hidden,
85+
list_subcommands=list_subcommands,
7086
has_attr_list=has_attr_list,
7187
)
7288

7389

90+
def _build_command_context(
91+
prog_name: str, command: click.BaseCommand, parent: Optional[click.Context]
92+
) -> click.Context:
93+
return click.Context(cast(click.Command, command), info_name=prog_name, parent=parent)
94+
95+
7496
def _get_sub_commands(command: click.Command, ctx: click.Context) -> List[click.Command]:
7597
"""Return subcommands of a Click command."""
7698
subcommands = getattr(command, "commands", {})
7799
if subcommands:
78-
return subcommands.values() # type: ignore
100+
return list(subcommands.values())
79101

80102
if not isinstance(command, click.MultiCommand):
81103
return []
@@ -131,26 +153,29 @@ def _make_description(ctx: click.Context, remove_ascii_art: bool = False) -> Ite
131153
"""Create markdown lines based on the command's own description."""
132154
help_string = ctx.command.help or ctx.command.short_help
133155

134-
if help_string:
135-
# https://github.com/pallets/click/pull/2151
136-
help_string = inspect.cleandoc(help_string)
137-
138-
if remove_ascii_art:
139-
skipped_ascii_art = True
140-
for i, line in enumerate(help_string.splitlines()):
141-
if skipped_ascii_art is False:
142-
if not line.strip():
143-
skipped_ascii_art = True
144-
continue
145-
elif i == 0 and line.strip() == "\b":
146-
skipped_ascii_art = False
147-
148-
if skipped_ascii_art:
149-
yield line
150-
else:
151-
yield from help_string.splitlines()
156+
if not help_string:
157+
return
158+
159+
# https://github.com/pallets/click/pull/2151
160+
help_string = inspect.cleandoc(help_string)
152161

162+
if not remove_ascii_art:
163+
yield from help_string.splitlines()
153164
yield ""
165+
return
166+
167+
skipped_ascii_art = True
168+
for i, line in enumerate(help_string.splitlines()):
169+
if skipped_ascii_art is False:
170+
if not line.strip():
171+
skipped_ascii_art = True
172+
continue
173+
elif i == 0 and line.strip() == "\b":
174+
skipped_ascii_art = False
175+
176+
if skipped_ascii_art:
177+
yield line
178+
yield ""
154179

155180

156181
def _make_usage(ctx: click.Context) -> Iterator[str]:
@@ -300,3 +325,27 @@ def _make_table_options(ctx: click.Context, show_hidden: bool = False) -> Iterat
300325
yield "| ---- | ---- | ----------- | ------- |"
301326
yield from option_rows
302327
yield ""
328+
329+
330+
def _make_subcommands_links(
331+
subcommands: List[click.Command],
332+
parent: click.Context,
333+
has_attr_list: bool,
334+
show_hidden: bool,
335+
) -> Iterator[str]:
336+
337+
yield "**Subcommands**"
338+
yield ""
339+
for command in subcommands:
340+
command_name = cast(str, command.name)
341+
ctx = _build_command_context(command_name, command, parent)
342+
if ctx.command.hidden and not show_hidden:
343+
continue
344+
command_bullet = command_name if not has_attr_list else f"[{command_name}](#{slugify(ctx.command_path, '-')})"
345+
help_string = ctx.command.short_help or ctx.command.help
346+
if help_string is not None:
347+
help_string = help_string.splitlines()[0]
348+
else:
349+
help_string = "*No description was provided with this command.*"
350+
yield f"- *{command_bullet}*: {help_string}"
351+
yield ""

mkdocs_click/_extension.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def replace_command_docs(has_attr_list: bool = False, **options: Any) -> Iterato
2525
style = options.get("style", "plain")
2626
remove_ascii_art = options.get("remove_ascii_art", False)
2727
show_hidden = options.get("show_hidden", False)
28+
list_subcommands = options.get("list_subcommands", False)
2829

2930
command_obj = load_command(module, command)
3031

@@ -37,6 +38,7 @@ def replace_command_docs(has_attr_list: bool = False, **options: Any) -> Iterato
3738
style=style,
3839
remove_ascii_art=remove_ascii_art,
3940
show_hidden=show_hidden,
41+
list_subcommands=list_subcommands,
4042
has_attr_list=has_attr_list,
4143
)
4244

mkdocs_click/_processing.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ def replace_blocks(lines: Iterable[str], title: str, replace: Callable[..., Iter
2727
# New ':key:' or ':key: value' line, ingest it.
2828
key = match.group("key")
2929
value = match.group("value") or ""
30+
if value.lower() in ["true", "false"]:
31+
value = True if value.lower() == "true" else False
3032
options[key] = value
3133
continue
3234

tests/app/expected-sub-enhanced.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# cli { #cli data-toc-label="cli" }
2+
3+
Main entrypoint for this dummy program
4+
5+
**Usage:**
6+
7+
```
8+
cli [OPTIONS] COMMAND [ARGS]...
9+
```
10+
11+
**Options:**
12+
13+
```
14+
--help Show this message and exit.
15+
```
16+
17+
**Subcommands**
18+
19+
- *[bar](#cli-bar)*: The bar command
20+
- *[foo](#cli-foo)*: *No description was provided with this command.*
21+
22+
## cli bar { #cli-bar data-toc-label="bar" }
23+
24+
The bar command
25+
26+
**Usage:**
27+
28+
```
29+
cli bar [OPTIONS] COMMAND [ARGS]...
30+
```
31+
32+
**Options:**
33+
34+
```
35+
--help Show this message and exit.
36+
```
37+
38+
**Subcommands**
39+
40+
- *[hello](#cli-bar-hello)*: Simple program that greets NAME for a total of COUNT times.
41+
42+
### cli bar hello { #cli-bar-hello data-toc-label="hello" }
43+
44+
Simple program that greets NAME for a total of COUNT times.
45+
46+
**Usage:**
47+
48+
```
49+
cli bar hello [OPTIONS]
50+
```
51+
52+
**Options:**
53+
54+
```
55+
--count INTEGER Number of greetings.
56+
--name TEXT The person to greet.
57+
--help Show this message and exit.
58+
```
59+
60+
## cli foo { #cli-foo data-toc-label="foo" }
61+
62+
**Usage:**
63+
64+
```
65+
cli foo [OPTIONS]
66+
```
67+
68+
**Options:**
69+
70+
```
71+
--help Show this message and exit.
72+
```

tests/app/expected-sub.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# cli
2+
3+
Main entrypoint for this dummy program
4+
5+
**Usage:**
6+
7+
```
8+
cli [OPTIONS] COMMAND [ARGS]...
9+
```
10+
11+
**Options:**
12+
13+
```
14+
--help Show this message and exit.
15+
```
16+
17+
**Subcommands**
18+
19+
- *bar*: The bar command
20+
- *foo*: *No description was provided with this command.*
21+
22+
## bar
23+
24+
The bar command
25+
26+
**Usage:**
27+
28+
```
29+
cli bar [OPTIONS] COMMAND [ARGS]...
30+
```
31+
32+
**Options:**
33+
34+
```
35+
--help Show this message and exit.
36+
```
37+
38+
**Subcommands**
39+
40+
- *hello*: Simple program that greets NAME for a total of COUNT times.
41+
42+
### hello
43+
44+
Simple program that greets NAME for a total of COUNT times.
45+
46+
**Usage:**
47+
48+
```
49+
cli bar hello [OPTIONS]
50+
```
51+
52+
**Options:**
53+
54+
```
55+
--count INTEGER Number of greetings.
56+
--name TEXT The person to greet.
57+
--help Show this message and exit.
58+
```
59+
60+
## foo
61+
62+
**Usage:**
63+
64+
```
65+
cli foo [OPTIONS]
66+
```
67+
68+
**Options:**
69+
70+
```
71+
--help Show this message and exit.
72+
```

0 commit comments

Comments
 (0)