-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
eee916e
commit adbc3b8
Showing
20 changed files
with
550 additions
and
361 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
# (C) Datadog, Inc. 2020-present | ||
# All rights reserved | ||
# Licensed under the Apache license (see LICENSE) | ||
from typing import Iterator, List, Optional, cast | ||
|
||
import click | ||
|
||
from ._exceptions import MkDocsClickException | ||
|
||
|
||
def make_command_docs(prog_name: str, command: click.BaseCommand, level: int = 0) -> Iterator[str]: | ||
"""Create the Markdown lines for a command and its sub-commands.""" | ||
for line in _recursively_make_command_docs(prog_name, command, level=level): | ||
yield line.replace("\b", "") | ||
|
||
|
||
def _recursively_make_command_docs( | ||
prog_name: str, command: click.BaseCommand, parent: click.Context = None, level: int = 0 | ||
) -> Iterator[str]: | ||
"""Create the raw Markdown lines for a command and its sub-commands.""" | ||
ctx = click.Context(cast(click.Command, command), parent=parent) | ||
|
||
yield from _make_title(prog_name, level) | ||
yield from _make_description(ctx) | ||
yield from _make_usage(ctx) | ||
yield from _make_options(ctx) | ||
|
||
subcommands = _get_sub_commands(ctx.command, ctx) | ||
|
||
for command in sorted(subcommands, key=lambda cmd: cmd.name): | ||
yield from _recursively_make_command_docs(command.name, command, parent=ctx, level=level + 1) | ||
|
||
|
||
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() | ||
|
||
if not isinstance(command, click.MultiCommand): | ||
return [] | ||
|
||
subcommands = [] | ||
|
||
for name in command.list_commands(ctx): | ||
subcommand = command.get_command(ctx, name) | ||
assert subcommand is not None | ||
subcommands.append(subcommand) | ||
|
||
return subcommands | ||
|
||
|
||
def _make_title(prog_name: str, level: int) -> Iterator[str]: | ||
"""Create the first markdown lines describing a command.""" | ||
yield _make_header(prog_name, level) | ||
yield "" | ||
|
||
|
||
def _make_header(text: str, level: int) -> str: | ||
"""Create a markdown header at a given level""" | ||
return f"{'#' * (level + 1)} {text}" | ||
|
||
|
||
def _make_description(ctx: click.Context) -> Iterator[str]: | ||
"""Create markdown lines based on the command's own description.""" | ||
help_string = ctx.command.help or ctx.command.short_help | ||
|
||
if help_string: | ||
yield from help_string.splitlines() | ||
yield "" | ||
|
||
|
||
def _make_usage(ctx: click.Context) -> Iterator[str]: | ||
"""Create the Markdown lines from the command usage string.""" | ||
|
||
# Gets the usual 'Usage' string without the prefix. | ||
formatter = ctx.make_formatter() | ||
pieces = ctx.command.collect_usage_pieces(ctx) | ||
formatter.write_usage(ctx.command_path, " ".join(pieces), prefix="") | ||
usage = formatter.getvalue().rstrip("\n") | ||
|
||
# Generate the full usage string based on parents if any, i.e. `root sub1 sub2 ...`. | ||
full_path = [] | ||
current: Optional[click.Context] = ctx | ||
while current is not None: | ||
name = current.command.name | ||
if name is None: | ||
raise MkDocsClickException(f"command {current.command} has no `name`") | ||
full_path.append(name) | ||
current = current.parent | ||
|
||
full_path.reverse() | ||
usage_snippet = " ".join(full_path) + usage | ||
|
||
yield "Usage:" | ||
yield "" | ||
yield "```" | ||
yield usage_snippet | ||
yield "```" | ||
yield "" | ||
|
||
|
||
def _make_options(ctx: click.Context) -> Iterator[str]: | ||
"""Create the Markdown lines describing the options for the command.""" | ||
formatter = ctx.make_formatter() | ||
click.Command.format_options(ctx.command, ctx, formatter) | ||
# First line is redundant "Options" | ||
# Last line is `--help` | ||
option_lines = formatter.getvalue().splitlines()[1:-1] | ||
if not option_lines: | ||
return | ||
|
||
yield "Options:" | ||
yield "" | ||
yield "```" | ||
yield from option_lines | ||
yield "```" | ||
yield "" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
class MkDocsClickException(Exception): | ||
""" | ||
Generic exception class for mkdocs-click errors. | ||
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# (C) Datadog, Inc. 2020-present | ||
# All rights reserved | ||
# Licensed under the Apache license (see LICENSE) | ||
from typing import Any, List, Iterator | ||
|
||
from markdown import Markdown | ||
from markdown.extensions import Extension | ||
from markdown.preprocessors import Preprocessor | ||
|
||
from ._docs import make_command_docs | ||
from ._exceptions import MkDocsClickException | ||
from ._loader import load_command | ||
from ._processing import replace_blocks | ||
|
||
|
||
def replace_command_docs(**options: Any) -> Iterator[str]: | ||
for option in ("module", "command"): | ||
if option not in options: | ||
raise MkDocsClickException(f"Option {option!r} is required") | ||
|
||
module = options["module"] | ||
command = options["command"] | ||
depth = int(options.get("depth", 0)) | ||
|
||
command_obj = load_command(module, command) | ||
|
||
return make_command_docs(prog_name=command, command=command_obj, level=depth) | ||
|
||
|
||
class ClickProcessor(Preprocessor): | ||
def run(self, lines: List[str]) -> List[str]: | ||
return list(replace_blocks(lines, title="mkdocs-click", replace=replace_command_docs)) | ||
|
||
|
||
class MKClickExtension(Extension): | ||
""" | ||
Replace blocks like the following: | ||
::: mkdocs-click | ||
:module: example.main | ||
:command: cli | ||
by Markdown documentation generated from the specified Click application. | ||
""" | ||
|
||
def extendMarkdown(self, md: Markdown) -> None: | ||
md.registerExtension(self) | ||
processor = ClickProcessor(md.parser) | ||
md.preprocessors.register(processor, "mk_click", 141) | ||
|
||
|
||
def makeExtension() -> Extension: | ||
return MKClickExtension() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# (C) Datadog, Inc. 2020-present | ||
# All rights reserved | ||
# Licensed under the Apache license (see LICENSE) | ||
import importlib | ||
from typing import Any | ||
|
||
import click | ||
from ._exceptions import MkDocsClickException | ||
|
||
|
||
def load_command(module: str, attribute: str) -> click.BaseCommand: | ||
""" | ||
Load and return the Click command object located at '<module>:<attribute>'. | ||
""" | ||
command = _load_obj(module, attribute) | ||
|
||
if not isinstance(command, click.BaseCommand): | ||
raise MkDocsClickException(f"{attribute!r} must be a 'click.BaseCommand' object, got {type(command)}") | ||
|
||
return command | ||
|
||
|
||
def _load_obj(module: str, attribute: str) -> Any: | ||
try: | ||
mod = importlib.import_module(module) | ||
except SystemExit: | ||
raise MkDocsClickException("the module appeared to call sys.exit()") # pragma: no cover | ||
|
||
try: | ||
return getattr(mod, attribute) | ||
except AttributeError: | ||
raise MkDocsClickException(f"Module {module!r} has no attribute {attribute!r}") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# (C) Datadog, Inc. 2020-present | ||
# All rights reserved | ||
# Licensed under the Apache license (see LICENSE) | ||
import re | ||
from typing import Callable, Iterable, Iterator | ||
|
||
|
||
def replace_blocks(lines: Iterable[str], title: str, replace: Callable[..., Iterable[str]]) -> Iterator[str]: | ||
""" | ||
Find blocks of lines in the form of: | ||
::: <title> | ||
:<key1>: <value> | ||
:<key2>: | ||
... | ||
And replace them with the lines returned by `replace(key1="<value1>", key2="", ...)`. | ||
""" | ||
|
||
options = {} | ||
in_block_section = False | ||
|
||
for line in lines: | ||
if in_block_section: | ||
match = re.search(r"^\s+:(?P<key>.+):(?:\s+(?P<value>\S+))?", line) | ||
if match is not None: | ||
# New ':key:' or ':key: value' line, ingest it. | ||
key = match.group("key") | ||
value = match.group("value") or "" | ||
options[key] = value | ||
continue | ||
|
||
# Block is finished, flush it. | ||
in_block_section = False | ||
yield from replace(**options) | ||
yield line | ||
continue | ||
|
||
match = re.search(rf"^::: {title}", line) | ||
if match is not None: | ||
# Block header, ingest it. | ||
in_block_section = True | ||
options = {} | ||
else: | ||
yield line |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.