Skip to content

Commit 12c10aa

Browse files
authored
chore: Fix failing tests (#436)
1 parent 3b8df3d commit 12c10aa

File tree

8 files changed

+108
-83
lines changed

8 files changed

+108
-83
lines changed

.github/workflows/cli.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
working-directory: ${{env.working-directory}}
5050
run: |
5151
uv run flake8 ./ --count --select=E9,F63,F7,F82 --show-source --statistics
52-
uv run flake8 ./ --count --exit-zero --max-complexity=10 --max-line-length=88 --statistics
52+
uv run flake8 ./ --count --exit-zero --max-complexity=10 --max-line-length=95 --statistics
5353
5454
- name: Run Static Checks
5555
working-directory: ${{env.working-directory}}

cli/.flake8

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
[flake8]
2-
max-line-length = 88
2+
max-line-length = 95
33
extend-ignore =
44
# E203: whitespace before ':' (conflicts with black)
5-
E203,
5+
E203,
66
# W503: line break before binary operator (conflicts with black)
77
W503,
88
# E402: module level import not at top (needed for sys.path manipulation)

cli/README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ The CLI will be installed as an editable package.
2121
```bash
2222
linux-cli --help
2323
```
24+
2425
### Options
2526
* `--verbose`: Enable verbose output for debugging purposes.
2627
Example:
@@ -72,9 +73,14 @@ python scripts/generate_command_index.py
7273

7374
That will create `cli/data/commands.json` which `show` and `search` will prefer when present. The test `tests/test_generate_index.py` exercises the generator.
7475

75-
7676
## Development
7777

78+
For development, ensure you have the development dependencies installed:
79+
80+
```bash
81+
uv sync --frozen --group dev
82+
```
83+
7884
### Running Tests
7985
```bash
8086
pytest
@@ -109,4 +115,4 @@ The CLI has its own workflow that runs:
109115
- Type checking (MyPy)
110116
- Tests (pytest)
111117

112-
This workflow only triggers on changes to the `cli/` directory.
118+
This workflow only triggers on changes to the `cli/` directory.

cli/commands/search.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
from typing import Dict, List
2-
31
import json
42
from pathlib import Path
3+
from typing import Dict, List
4+
55
import typer
66

77
from states.global_state import debug, verbose_flag
88

9-
109
app = typer.Typer(help=("Search available commands by keyword (name or description)."))
1110

1211

cli/commands/show.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
from typing import Dict, TypedDict
2-
31
import json
42
from pathlib import Path
3+
from typing import Dict, TypedDict
4+
55
import typer
66

77
from states.global_state import debug, verbose_flag

cli/scripts/generate_command_index.py

Lines changed: 90 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -5,116 +5,136 @@
55
from each markdown file under ebook/en/content. Writes output to cli/data/commands.json
66
"""
77

8-
from pathlib import Path
9-
import re
108
import json
9+
import re
1110
import sys
11+
from pathlib import Path
1212

1313

14-
def extract_from_markdown(path: Path) -> dict:
15-
text = path.read_text(encoding="utf-8")
16-
lines = text.splitlines()
17-
18-
# title: first heading line starting with '#'
19-
title = None
14+
def _extract_title(lines: list[str]) -> str | None:
15+
"""Extract the first heading line starting with '#'."""
2016
for ln in lines:
2117
if ln.strip().startswith("#"):
22-
title = ln.strip().lstrip("#").strip()
23-
break
18+
return ln.strip().lstrip("#").strip()
19+
return None
20+
2421

25-
# try to infer command name from code span in title like `ls`
26-
name = None
22+
def _extract_command_name(title: str | None, path: Path) -> str:
23+
"""Extract command name from title or fallback to filename pattern."""
2724
if title:
25+
# Try to infer command name from code span in title like `ls`
2826
m = re.search(r"`([a-zA-Z0-9_\-]+)`", title)
2927
if m:
30-
name = m.group(1)
31-
else:
32-
# fallback: take last token of title
33-
tokens = re.findall(r"\w+", title)
34-
if tokens:
35-
name = tokens[-1].lower()
36-
37-
# fallback: filename pattern 001-the-ls-command.md -> ls
38-
if not name:
39-
m = re.search(r"-the-([a-zA-Z0-9_\-]+)-command", path.name)
40-
if m:
41-
name = m.group(1)
42-
else:
43-
name = path.stem
28+
return m.group(1)
29+
# Fallback: take last token of title
30+
tokens = re.findall(r"\w+", title)
31+
if tokens:
32+
return tokens[-1].lower()
33+
34+
# Fallback: filename pattern 001-the-ls-command.md -> ls
35+
m = re.search(r"-the-([a-zA-Z0-9_\-]+)-command", path.name)
36+
if m:
37+
return m.group(1)
38+
39+
return path.stem
40+
4441

45-
# description: first paragraph after title
46-
desc = ""
42+
def _extract_description(lines: list[str]) -> str:
43+
"""Extract first paragraph after title."""
4744
try:
4845
idx = 0
4946
while idx < len(lines) and not lines[idx].strip().startswith("#"):
5047
idx += 1
51-
# move to next line after title
48+
# Move to next line after title
5249
idx += 1
53-
# collect lines until empty line
50+
# Collect lines until empty line
5451
para = []
5552
while idx < len(lines):
56-
if lines[idx].strip() == "":
53+
line = lines[idx].strip()
54+
if line == "":
5755
if para:
5856
break
5957
idx += 1
6058
continue
61-
if lines[idx].strip().startswith("###") or lines[idx].strip().startswith(
62-
"##"
63-
):
59+
if line.startswith("###") or line.startswith("##"):
6460
if para:
6561
break
66-
para.append(lines[idx].strip())
62+
para.append(line)
6763
idx += 1
68-
desc = " ".join(para).strip()
64+
return " ".join(para).strip()
6965
except Exception:
70-
desc = ""
66+
return ""
7167

72-
# usage: look for 'Syntax' or 'Usage' section code block
73-
usage = ""
68+
69+
def _extract_usage(text: str) -> str:
70+
"""Extract usage information from Syntax or Usage sections."""
7471
usage_match = re.search(
7572
r"###\s*Syntax\s*:\s*\n(```[\s\S]*?```)", text, flags=re.IGNORECASE
7673
)
7774
if usage_match:
78-
usage = usage_match.group(1).strip().strip("`")
79-
else:
80-
m2 = re.search(r"Usage\s*[:|-]\s*(.+)", text, flags=re.IGNORECASE)
81-
if m2:
82-
usage = m2.group(1).strip()
83-
84-
# example: prefer first fenced code block under Examples or first code block
85-
example = ""
75+
return usage_match.group(1).strip().strip("`")
76+
77+
m2 = re.search(r"Usage\s*[:|-]\s*(.+)", text, flags=re.IGNORECASE)
78+
if m2:
79+
return m2.group(1).strip()
80+
81+
return ""
82+
83+
84+
def _extract_example(text: str) -> str:
85+
"""Extract example from Examples section or first code block."""
8686
examples_section = re.search(
8787
r"###\s*Examples[:\s]*\n([\s\S]*?)(?:\n###|\n##|$)", text, flags=re.IGNORECASE
8888
)
89-
if examples_section:
90-
# find first fenced code block inside
91-
fb = re.search(
92-
r"```(?:bash|sh|shell|).*?\n([\s\S]*?)```",
93-
examples_section.group(1),
94-
flags=re.IGNORECASE,
95-
)
96-
if fb:
97-
example = fb.group(1).strip()
98-
else:
99-
# fallback: first inline code occurrence
100-
ic = re.search(r"`([^`]+)`", examples_section.group(1))
101-
if ic:
102-
example = ic.group(1).strip()
103-
104-
# notes: capture 'Additional Flags' or remaining list items
105-
notes = ""
89+
if not examples_section:
90+
return ""
91+
92+
# Find first fenced code block inside
93+
fb = re.search(
94+
r"```(?:bash|sh|shell|).*?\n([\s\S]*?)```",
95+
examples_section.group(1),
96+
flags=re.IGNORECASE,
97+
)
98+
if fb:
99+
return fb.group(1).strip()
100+
101+
# Fallback: first inline code occurrence
102+
ic = re.search(r"`([^`]+)`", examples_section.group(1))
103+
if ic:
104+
return ic.group(1).strip()
105+
106+
return ""
107+
108+
109+
def _extract_notes(text: str) -> str:
110+
"""Extract notes from Additional Flags/Notes sections or bullet points."""
106111
notes_section = re.search(
107112
r"###\s*(Additional Flags|Notes|Notes:|Additional).*?\n([\s\S]*?)(?:\n###|\n##|$)",
108113
text,
109114
flags=re.IGNORECASE,
110115
)
111116
if notes_section:
112-
notes = notes_section.group(2).strip()
113-
else:
114-
# collect bullet points
115-
bullets = re.findall(r"^\s*[-\*]\s+(.+)$", text, flags=re.MULTILINE)
116-
if bullets:
117-
notes = "; ".join(bullets[:10])
117+
return notes_section.group(2).strip()
118+
119+
# Collect bullet points
120+
bullets = re.findall(r"^\s*[-\*]\s+(.+)$", text, flags=re.MULTILINE)
121+
if bullets:
122+
return "; ".join(bullets[:10])
123+
124+
return ""
125+
126+
127+
def extract_from_markdown(path: Path) -> dict:
128+
"""Extract command information from a markdown file."""
129+
text = path.read_text(encoding="utf-8")
130+
lines = text.splitlines()
131+
132+
title = _extract_title(lines)
133+
name = _extract_command_name(title, path)
134+
desc = _extract_description(lines)
135+
usage = _extract_usage(text)
136+
example = _extract_example(text)
137+
notes = _extract_notes(text)
118138

119139
return {
120140
"name": name,

cli/tests/test_cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def clean_output(text: str) -> str:
4949
),
5050
(("show", "ls"), ("ls", "List"), 0),
5151
(("--verbose", "show", "ls"), ("[DEBUG] Showing details for command: ls"), 0),
52-
(("show", "grep"), ("Search",), 0),
52+
(("show", "grep"), ("searches",), 0),
5353
(
5454
("--verbose", "show", "grep"),
5555
("[DEBUG] Showing details for command: grep"),

cli/tests/test_generate_index.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
#!/usr/bin/env python3
22
"""Tests for the generator script that builds cli/data/commands.json."""
33

4-
from pathlib import Path
4+
import json
55
import subprocess
66
import sys
7-
import json
7+
from pathlib import Path
88

99

1010
def test_generate_index_creates_file():

0 commit comments

Comments
 (0)