66import re
77import sys
88from pathlib import Path
9+ from typing import Iterable
910
1011
1112ROOT = 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+
74162def 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