Skip to content

Commit 215bc50

Browse files
authored
fix(pretty): Fix bug where commands and options were displayed in the… (#17)
## Pull Request: Add Support for Parsing and Rendering Command Tables in Markdown CLI Docs ### Summary This PR enhances the `pretty.py` module to support parsing and rendering a **Commands** section in CLI documentation. The changes allow the system to recognize, store, and output tables of subcommands (with names and descriptions) in both the markdown-to-tree and tree-to-markdown conversions. ### Details - **New Model:** - Added a `CommandEntry` Pydantic model to represent individual commands with `name` and `description` fields. - Updated the `CommandNode` model to include a `commands: List[CommandEntry]` field. - **Markdown Parsing (`parse_markdown_to_tree`):** - Recognizes the `**Commands**:` section in markdown. - Parses each command entry (name and description) and adds it to the `commands` list of the current command node. - Handles both the standard `* `name`: description` and fallback `* `name`` formats. - **Markdown Rendering (`tree_to_markdown`):** - Adds a "Commands" table to the generated markdown if any commands are present. - Formats the table with columns for "Name" and "Description". - **Other Improvements:** - Ensures section state is correctly managed when switching between arguments, options, and commands. - Adds tests for edge cases in command parsing and rendering. ### Motivation This change makes the CLI documentation more comprehensive by including a structured list of subcommands, improving both the parsing of existing markdown and the generation of new documentation. ### Example A markdown section like: ```markdown **Commands**: * `serve`: Start the server * `build`: Build the project ``` Will now be parsed into the tree and rendered back as a markdown table. --- **Closes:** #16
1 parent a384c2a commit 215bc50

File tree

5 files changed

+82
-3
lines changed

5 files changed

+82
-3
lines changed

.coverage

0 Bytes
Binary file not shown.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "mkdocs-typer2"
3-
version = "0.1.4"
3+
version = "0.1.5"
44
description = "Mkdocs plugin for generating Typer CLI docs"
55
readme = "README.md"
66
requires-python = ">=3.10"

src/mkdocs_typer2/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def docs(name: str = typer.Option(..., help="The name of the project")):
1111
print(f"Generating docs for {name}")
1212

1313

14-
@app.command()
14+
@app.command(hidden=True)
1515
def hello(
1616
name: Annotated[str, typer.Argument(..., help="The name of the person to greet")],
1717
caps: Annotated[

src/mkdocs_typer2/pretty.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@ class Argument(BaseModel):
1818
required: bool = False
1919

2020

21+
class CommandEntry(BaseModel):
22+
name: str
23+
description: str = ""
24+
25+
2126
class CommandNode(BaseModel):
2227
name: str
2328
description: str = ""
2429
usage: Optional[str] = None
2530
arguments: List[Argument] = []
2631
options: List[Option] = []
2732
subcommands: List["CommandNode"] = []
33+
commands: List[CommandEntry] = []
2834

2935

3036
def parse_markdown_to_tree(content: str) -> CommandNode:
@@ -68,6 +74,7 @@ def parse_markdown_to_tree(content: str) -> CommandNode:
6874
current_section = None
6975
in_description = False
7076
desc_lines = []
77+
in_commands_section = False
7178

7279
i = 0
7380
while i < len(lines):
@@ -81,6 +88,7 @@ def parse_markdown_to_tree(content: str) -> CommandNode:
8188
current_command = new_cmd
8289
in_description = True # Start capturing description for this command
8390
desc_lines = []
91+
in_commands_section = False
8492

8593
elif (
8694
in_description
@@ -97,6 +105,7 @@ def parse_markdown_to_tree(content: str) -> CommandNode:
97105
if in_description and desc_lines:
98106
current_command.description = "\n".join(desc_lines)
99107
in_description = False
108+
in_commands_section = False
100109
# Find the line with usage example
101110
j = i
102111
while j < len(lines) and "$ " not in lines[j]:
@@ -110,19 +119,23 @@ def parse_markdown_to_tree(content: str) -> CommandNode:
110119
current_command.description = "\n".join(desc_lines)
111120
in_description = False
112121
current_section = "arguments"
122+
in_commands_section = False
113123

114124
elif line.startswith("**Options**:"):
115125
# End of description section
116126
if in_description and desc_lines:
117127
current_command.description = "\n".join(desc_lines)
118128
in_description = False
119129
current_section = "options"
130+
in_commands_section = False
120131

121132
elif line.startswith("**Commands**:"):
122-
# End of description section
133+
# End of description section, start commands section
123134
if in_description and desc_lines:
124135
current_command.description = "\n".join(desc_lines)
125136
in_description = False
137+
current_section = None
138+
in_commands_section = True
126139

127140
elif line.startswith("* `") and current_section == "options":
128141
# Parse option
@@ -147,6 +160,23 @@ def parse_markdown_to_tree(content: str) -> CommandNode:
147160
)
148161
current_command.arguments.append(argument)
149162

163+
elif line.startswith("* `") and in_commands_section:
164+
# Parse command name and description
165+
match = re.match(r"\* `(.*?)`: (.*)", line)
166+
if match:
167+
cmd_name, cmd_desc = match.groups()
168+
current_command.commands.append(
169+
CommandEntry(name=cmd_name, description=cmd_desc.strip())
170+
)
171+
else:
172+
# Fallback: just the name
173+
match = re.match(r"\* `(.*?)`:?", line)
174+
if match:
175+
cmd_name = match.group(1)
176+
current_command.commands.append(
177+
CommandEntry(name=cmd_name, description="")
178+
)
179+
150180
i += 1
151181

152182
return root
@@ -191,6 +221,14 @@ def format_options_table(options: list[Option]) -> str:
191221

192222
return "\n".join(rows)
193223

224+
def format_commands_table(commands: list[CommandEntry]) -> str:
225+
if not commands:
226+
return "*No commands available*"
227+
rows = [format_table_row("Name", "Description"), format_table_row("---", "---")]
228+
for cmd in commands:
229+
rows.append(format_table_row(f"`{cmd.name}`", cmd.description))
230+
return "\n".join(rows)
231+
194232
def format_usage(cmd_name: str, usage: Optional[str]) -> str:
195233
if not usage:
196234
return "*No usage specified*"
@@ -213,6 +251,9 @@ def format_usage(cmd_name: str, usage: Optional[str]) -> str:
213251
"",
214252
"## Options\n",
215253
format_options_table(command_node.options),
254+
"",
255+
"## Commands\n",
256+
format_commands_table(command_node.commands),
216257
]
217258

218259
# Add subcommands if they exist

tests/test_pretty.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from mkdocs_typer2.pretty import (
44
Argument,
5+
CommandEntry,
56
CommandNode,
67
Option,
78
parse_markdown_to_tree,
@@ -245,3 +246,40 @@ def test_parse_typer_generated_docs():
245246
assert len(hello_cmd.options) > 0
246247
assert any(arg.name == "NAME" for arg in hello_cmd.arguments)
247248
assert any(opt.name == "--caps / --no-caps" for opt in hello_cmd.options)
249+
250+
251+
def test_parse_markdown_with_commands():
252+
markdown = """
253+
# mycli
254+
255+
**Commands**:
256+
* `foo`: Foo command description
257+
* `bar`: Bar command description
258+
"""
259+
result = parse_markdown_to_tree(markdown)
260+
assert len(result.commands) == 2
261+
assert result.commands[0].name == "foo"
262+
assert result.commands[0].description == "Foo command description"
263+
assert result.commands[1].name == "bar"
264+
assert result.commands[1].description == "Bar command description"
265+
266+
267+
def test_tree_to_markdown_with_commands():
268+
cmd = CommandNode(
269+
name="mycli",
270+
description="A test CLI",
271+
commands=[
272+
CommandEntry(name="foo", description="Foo command description"),
273+
CommandEntry(name="bar", description="Bar command description"),
274+
],
275+
)
276+
markdown = tree_to_markdown(cmd)
277+
assert "| Name | Description |" in markdown
278+
assert "`foo`" in markdown and "Foo command description" in markdown
279+
assert "`bar`" in markdown and "Bar command description" in markdown
280+
281+
282+
def test_tree_to_markdown_no_commands():
283+
cmd = CommandNode(name="mycli", description="A test CLI", commands=[])
284+
markdown = tree_to_markdown(cmd)
285+
assert "*No commands available*" in markdown

0 commit comments

Comments
 (0)