Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add link to subcommands #55

Merged
merged 6 commits into from
Jun 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,5 @@ Options:
- `style`: _(Optional, default: `plain`)_ Style for the options section. The possible choices are `plain` and `table`.
- `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.
- `show_hidden`: _(Optional, default: `False`)_ Show commands and options that are marked as hidden.
- `list_subcommands`: _(Optional, default: `False`)_ List subcommands of a given command. If _attr_list_ is installed,
add links to subcommands also.
93 changes: 71 additions & 22 deletions mkdocs_click/_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Licensed under the Apache license (see LICENSE)
import inspect
from contextlib import contextmanager, ExitStack
from typing import Iterator, List, cast
from typing import Iterator, List, cast, Optional

import click
from markdown.extensions.toc import slugify
Expand All @@ -18,6 +18,7 @@ def make_command_docs(
style: str = "plain",
remove_ascii_art: bool = False,
show_hidden: bool = False,
list_subcommands: bool = False,
has_attr_list: bool = False,
) -> Iterator[str]:
"""Create the Markdown lines for a command and its sub-commands."""
Expand All @@ -28,6 +29,7 @@ def make_command_docs(
style=style,
remove_ascii_art=remove_ascii_art,
show_hidden=show_hidden,
list_subcommands=list_subcommands,
has_attr_list=has_attr_list,
):
if line.strip() == "\b":
Expand All @@ -44,10 +46,11 @@ def _recursively_make_command_docs(
style: str = "plain",
remove_ascii_art: bool = False,
show_hidden: bool = False,
list_subcommands: bool = False,
has_attr_list: bool = False,
) -> Iterator[str]:
"""Create the raw Markdown lines for a command and its sub-commands."""
ctx = click.Context(cast(click.Command, command), info_name=prog_name, parent=parent)
ctx = _build_command_context(prog_name=prog_name, command=command, parent=parent)

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

subcommands = _get_sub_commands(ctx.command, ctx)
if len(subcommands) == 0:
return

subcommands.sort(key=lambda cmd: str(cmd.name))

if list_subcommands:
yield from _make_subcommands_links(
subcommands,
ctx,
has_attr_list=has_attr_list,
show_hidden=show_hidden,
)

for command in sorted(subcommands, key=lambda cmd: cmd.name): # type: ignore
for command in subcommands:
yield from _recursively_make_command_docs(
cast(str, command.name),
command,
parent=ctx,
depth=depth + 1,
style=style,
show_hidden=show_hidden,
list_subcommands=list_subcommands,
has_attr_list=has_attr_list,
)


def _build_command_context(
prog_name: str, command: click.BaseCommand, parent: Optional[click.Context]
) -> click.Context:
return click.Context(cast(click.Command, command), info_name=prog_name, parent=parent)


def _get_sub_commands(command: click.Command, ctx: click.Context) -> List[click.Command]:
"""Return subcommands of a Click command."""
subcommands = getattr(command, "commands", {})
if subcommands:
return subcommands.values() # type: ignore
return list(subcommands.values())

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

if help_string:
# https://github.com/pallets/click/pull/2151
help_string = inspect.cleandoc(help_string)

if remove_ascii_art:
skipped_ascii_art = True
for i, line in enumerate(help_string.splitlines()):
if skipped_ascii_art is False:
if not line.strip():
skipped_ascii_art = True
continue
elif i == 0 and line.strip() == "\b":
skipped_ascii_art = False

if skipped_ascii_art:
yield line
else:
yield from help_string.splitlines()
if not help_string:
return

# https://github.com/pallets/click/pull/2151
help_string = inspect.cleandoc(help_string)

if not remove_ascii_art:
yield from help_string.splitlines()
yield ""
return

skipped_ascii_art = True
for i, line in enumerate(help_string.splitlines()):
if skipped_ascii_art is False:
if not line.strip():
skipped_ascii_art = True
continue
elif i == 0 and line.strip() == "\b":
skipped_ascii_art = False

if skipped_ascii_art:
yield line
yield ""


def _make_usage(ctx: click.Context) -> Iterator[str]:
Expand Down Expand Up @@ -300,3 +325,27 @@ def _make_table_options(ctx: click.Context, show_hidden: bool = False) -> Iterat
yield "| ---- | ---- | ----------- | ------- |"
yield from option_rows
yield ""


def _make_subcommands_links(
subcommands: List[click.Command],
parent: click.Context,
has_attr_list: bool,
show_hidden: bool,
) -> Iterator[str]:

yield "**Subcommands**"
yield ""
for command in subcommands:
command_name = cast(str, command.name)
ctx = _build_command_context(command_name, command, parent)
if ctx.command.hidden and not show_hidden:
continue
command_bullet = command_name if not has_attr_list else f"[{command_name}](#{slugify(ctx.command_path, '-')})"
help_string = ctx.command.short_help or ctx.command.help
if help_string is not None:
help_string = help_string.splitlines()[0]
else:
help_string = "*No description was provided with this command.*"
yield f"- *{command_bullet}*: {help_string}"
yield ""
2 changes: 2 additions & 0 deletions mkdocs_click/_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def replace_command_docs(has_attr_list: bool = False, **options: Any) -> Iterato
style = options.get("style", "plain")
remove_ascii_art = options.get("remove_ascii_art", False)
show_hidden = options.get("show_hidden", False)
list_subcommands = options.get("list_subcommands", False)

command_obj = load_command(module, command)

Expand All @@ -37,6 +38,7 @@ def replace_command_docs(has_attr_list: bool = False, **options: Any) -> Iterato
style=style,
remove_ascii_art=remove_ascii_art,
show_hidden=show_hidden,
list_subcommands=list_subcommands,
has_attr_list=has_attr_list,
)

Expand Down
2 changes: 2 additions & 0 deletions mkdocs_click/_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ def replace_blocks(lines: Iterable[str], title: str, replace: Callable[..., Iter
# New ':key:' or ':key: value' line, ingest it.
key = match.group("key")
value = match.group("value") or ""
if value.lower() in ["true", "false"]:
value = True if value.lower() == "true" else False
options[key] = value
continue

Expand Down
72 changes: 72 additions & 0 deletions tests/app/expected-sub-enhanced.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# cli { #cli data-toc-label="cli" }

Main entrypoint for this dummy program

**Usage:**

```
cli [OPTIONS] COMMAND [ARGS]...
```

**Options:**

```
--help Show this message and exit.
```

**Subcommands**

- *[bar](#cli-bar)*: The bar command
- *[foo](#cli-foo)*: *No description was provided with this command.*

## cli bar { #cli-bar data-toc-label="bar" }

The bar command

**Usage:**

```
cli bar [OPTIONS] COMMAND [ARGS]...
```

**Options:**

```
--help Show this message and exit.
```

**Subcommands**

- *[hello](#cli-bar-hello)*: Simple program that greets NAME for a total of COUNT times.

### cli bar hello { #cli-bar-hello data-toc-label="hello" }

Simple program that greets NAME for a total of COUNT times.

**Usage:**

```
cli bar hello [OPTIONS]
```

**Options:**

```
--count INTEGER Number of greetings.
--name TEXT The person to greet.
--help Show this message and exit.
```

## cli foo { #cli-foo data-toc-label="foo" }

**Usage:**

```
cli foo [OPTIONS]
```

**Options:**

```
--help Show this message and exit.
```
72 changes: 72 additions & 0 deletions tests/app/expected-sub.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# cli

Main entrypoint for this dummy program

**Usage:**

```
cli [OPTIONS] COMMAND [ARGS]...
```

**Options:**

```
--help Show this message and exit.
```

**Subcommands**

- *bar*: The bar command
- *foo*: *No description was provided with this command.*

## bar

The bar command

**Usage:**

```
cli bar [OPTIONS] COMMAND [ARGS]...
```

**Options:**

```
--help Show this message and exit.
```

**Subcommands**

- *hello*: Simple program that greets NAME for a total of COUNT times.

### hello

Simple program that greets NAME for a total of COUNT times.

**Usage:**

```
cli bar hello [OPTIONS]
```

**Options:**

```
--count INTEGER Number of greetings.
--name TEXT The person to greet.
--help Show this message and exit.
```

## foo

**Usage:**

```
cli foo [OPTIONS]
```

**Options:**

```
--help Show this message and exit.
```
Loading