Skip to content

Commit 0ce8c63

Browse files
committed
ci: enforce docs structure and API reference coverage checks
1 parent 0c7220d commit 0ce8c63

1 file changed

Lines changed: 90 additions & 0 deletions

File tree

scripts/check_version_consistency.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import re
77
import sys
88
from pathlib import Path
9+
from typing import Iterable
910

1011

1112
ROOT = Path(__file__).resolve().parents[1]
@@ -71,6 +72,93 @@ def _check_runtime_version(version: str, strict: bool) -> None:
7172
print(f"[WARN] Runtime check skipped: {exc}")
7273

7374

75+
def _extract_names_from_c_array(text: str, array_name: str) -> list[str]:
76+
pattern = rf"static\s+Py(?:MethodDef|MemberDef|GetSetDef)\s+{re.escape(array_name)}\[\]\s*=\s*\{{(.*?)\n\}};"
77+
match = re.search(pattern, text, re.DOTALL)
78+
if not match:
79+
_fail(f"Cannot find C array: {array_name}")
80+
body = match.group(1)
81+
body = re.sub(r"/\*.*?\*/", "", body, flags=re.DOTALL)
82+
body = re.sub(r"//.*", "", body)
83+
names = re.findall(r'\{\s*"([^"]+)"', body)
84+
return [n for n in names if n != "NULL"]
85+
86+
87+
def _extract_connect_keywords(connection_text: str) -> list[str]:
88+
match = re.search(
89+
r"static char \*keywordList\[\]\s*=\s*\{(.*?)NULL\s*\};",
90+
connection_text,
91+
re.DOTALL,
92+
)
93+
if not match:
94+
_fail("Cannot find connect() keywordList in Connection.c")
95+
return re.findall(r'"([^"]+)"', match.group(1))
96+
97+
98+
def _missing_tokens(doc_text: str, tokens: Iterable[str]) -> list[str]:
99+
missing = [t for t in tokens if t not in doc_text]
100+
return sorted(set(missing))
101+
102+
103+
def _check_docs_structure() -> None:
104+
required = [
105+
"docs/index.md",
106+
"docs/installation.md",
107+
"docs/quickstart.md",
108+
"docs/api-reference.md",
109+
"docs/examples/basic-crud.md",
110+
"docs/examples/bulk-insert.md",
111+
"docs/examples/lob-handling.md",
112+
"docs/examples/stored-proc.md",
113+
"docs/examples/connection-pool.md",
114+
"docs/migration.md",
115+
"docs/faq.md",
116+
"mkdocs.yml",
117+
]
118+
missing = [path for path in required if not (ROOT / path).exists()]
119+
if missing:
120+
_fail(f"Missing required docs files: {missing}")
121+
122+
123+
def _check_api_docs_coverage() -> None:
124+
api_doc = (ROOT / "docs/api-reference.md").read_text(encoding="utf-8")
125+
# Native C sources contain mixed legacy comments; decode losslessly.
126+
conn_c = (ROOT / "src/native/Connection.c").read_text(encoding="latin-1")
127+
cur_c = (ROOT / "src/native/Cursor.c").read_text(encoding="latin-1")
128+
mod_c = (ROOT / "src/native/py_Dameng.c").read_text(encoding="latin-1")
129+
130+
connect_keywords = _extract_connect_keywords(conn_c)
131+
conn_methods = _extract_names_from_c_array(conn_c, "g_ConnectionMethods")
132+
conn_members = _extract_names_from_c_array(conn_c, "g_ConnectionMembers")
133+
conn_getset = _extract_names_from_c_array(conn_c, "g_ConnectionCalcMembers")
134+
cur_methods = _extract_names_from_c_array(cur_c, "g_CursorMethods")
135+
cur_members = _extract_names_from_c_array(cur_c, "g_CursorMembers")
136+
cur_getset = _extract_names_from_c_array(cur_c, "g_CursorCalcMembers")
137+
mod_methods = _extract_names_from_c_array(mod_c, "g_ModuleMethods")
138+
exceptions = re.findall(r'SetException\(module,\s*&g_\w+,\s*"([^"]+)"', mod_c)
139+
140+
conn_getset = [n for n in conn_getset if not n.startswith("DSQL_ATTR_")]
141+
142+
checks = {
143+
"connect keywords": connect_keywords,
144+
"Connection methods": conn_methods,
145+
"Connection members": conn_members,
146+
"Connection get/set attrs": conn_getset,
147+
"Cursor methods": cur_methods,
148+
"Cursor members": cur_members,
149+
"Cursor get/set attrs": cur_getset,
150+
"module methods": mod_methods,
151+
"exceptions": exceptions,
152+
}
153+
errors: list[str] = []
154+
for section, tokens in checks.items():
155+
missing = _missing_tokens(api_doc, tokens)
156+
if missing:
157+
errors.append(f"{section}: {missing}")
158+
if errors:
159+
_fail("API docs coverage missing entries:\n" + "\n".join(errors))
160+
161+
74162
def main() -> int:
75163
parser = argparse.ArgumentParser(description=__doc__)
76164
parser.add_argument("--strict-runtime", action="store_true", help="Fail when runtime dmPython check is unavailable")
@@ -88,6 +176,8 @@ def main() -> int:
88176
_fail(f"Header version mismatch: {header_version} != {version}")
89177

90178
_check_docs_version(version)
179+
_check_docs_structure()
180+
_check_api_docs_coverage()
91181
_check_smoke_script_dynamic()
92182
_check_runtime_version(version, strict=args.strict_runtime)
93183

0 commit comments

Comments
 (0)