Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
29 changes: 29 additions & 0 deletions tests/test_readme_flag_audit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import sys
import tempfile
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))

from readme_flag_audit import build


class ReadmeFlagAuditTest(unittest.TestCase):
def test_detects_missing_readme_flags(self):
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
package = root / "mx_exporter"
package.mkdir()
(package / "__init__.py").write_text(
'parser.add_argument("-p", "--port")\nparser.add_argument("-i", "--interval")\n',
encoding="utf-8",
)
(root / "README.md").write_text("python3 -m mx_exporter -p 8000\n", encoding="utf-8")

report = build(root)

self.assertIn("--interval", report["flags_missing_from_readme"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

为了防止未来引入类似的回归问题,建议在测试中加入包含连字符单词(如 mx-exporter)的场景,并断言这些单词不会被错误地识别为命令行参数。

            (root / "README.md").write_text("python3 -m mx-exporter -p 8000\n", encoding="utf-8")\n\n            report = build(root)\n\n        self.assertIn("--interval", report["flags_missing_from_readme"])\n        self.assertNotIn("-exporter", report["flags_only_in_readme"])



if __name__ == "__main__":
unittest.main()
60 changes: 60 additions & 0 deletions tools/readme_flag_audit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""Compare CLI flags with README examples and parameter notes."""

from __future__ import annotations

import argparse
import ast
import json
import re
from pathlib import Path


FLAG_RE = re.compile(r"-{1,2}[A-Za-z0-9-]+")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

当前的正则表达式 -{1,2}[A-Za-z0-9-]+ 会匹配任何包含连字符的单词的后半部分。例如,在 README.md 中存在的 mx-exporter 会被匹配出 -exporterpod-resources 会被匹配出 -resourcesdefault-counters 会被匹配出 -counters。这会导致 flags_only_in_readme 中包含大量错误的“仅在 README 中存在的参数”假阳性结果。\n\n建议将正则表达式修改为仅匹配以空格开头或位于行首的命令行参数,例如使用非捕获分组 (?:^|\\s) 结合捕获分组来精确提取参数。

Suggested change
FLAG_RE = re.compile(r"-{1,2}[A-Za-z0-9-]+")
FLAG_RE = re.compile(r"(?:^|\s)(-{1,2}[A-Za-z0-9-]+)")



def cli_flags(init_path: Path) -> set[str]:
module = ast.parse(init_path.read_text(encoding="utf-8"), filename=str(init_path))
flags = set()
for node in ast.walk(module):
if not isinstance(node, ast.Call):
continue
if not isinstance(node.func, ast.Attribute) or node.func.attr != "add_argument":
continue
for argument in node.args:
if isinstance(argument, ast.Constant) and isinstance(argument.value, str):
flags.add(argument.value)
return flags


def readme_flags(readme_path: Path) -> set[str]:
return set(FLAG_RE.findall(readme_path.read_text(encoding="utf-8")))


def build(repo_root: Path) -> dict[str, object]:
cli = cli_flags(repo_root / "mx_exporter" / "__init__.py")
readme = readme_flags(repo_root / "README.md")
return {
"cli_flag_count": len(cli),
"readme_flag_count": len(readme),
"flags_missing_from_readme": sorted(flag for flag in cli - readme if flag.startswith("-")),
"flags_only_in_readme": sorted(flag for flag in readme - cli if flag.startswith("-")),
}


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, default=Path("."))
parser.add_argument("--output", type=Path)
args = parser.parse_args()

text = json.dumps(build(args.repo_root), indent=2, ensure_ascii=False)
if args.output:
args.output.write_text(text + "\n", encoding="utf-8")
else:
print(text)
return 0


if __name__ == "__main__":
raise SystemExit(main())