Skip to content

Commit cbb8208

Browse files
committed
Refactor AST emitters/parsers for bi-directional IR symmetry, strict typing, and 100% test/doc coverage
- [.gitignore, .coveragerc]: Anchor root-level ignore rules (/client.py, /src/client.py) so src/openapi_client/mcp_client/client.py is tracked; configure branch coverage across src/openapi_client and scripts without scratch-file omissions. - [src/openapi_client/openapi/keywords.py]: Deduplicate ~3,900 lines of copy-pasted _OPENAPI_3_2_0_FIELDS and _ALL_KEYWORDS tuples across 17 modules into canonical OPENAPI_3_2_0_FIELDS and ALL_KEYWORDS constants. - [src/openapi_client/classes/{emit,parse}.py]: Support PEP 604 union (T | None), Optional[T], list[T], dict[str, T], required vs optional fields, and $ref schema resolution in both Pydantic emission and LibCST ClassExtractor. - [src/openapi_client/functions/{emit,parse,utils}.py]: Extract HTTP verb and URL path from client method bodies (self.base_url + ... and self.http.request) in FunctionExtractor so custom operationIds round-trip; guard query string concatenation against trailing '?' when query_params is empty; add get_default_server_url helper. - [src/openapi_client/fastapi/{emit,parse}.py]: Construct typed PathItem and Operation Pydantic models instead of raw dicts in FastAPIExtractor; support both @app.<method> and @router.<method> decorators; sanitize route tags into valid Python module identifiers; emit 422 JSONResponse when strict_validation is enabled. - [src/openapi_client/cli_sdk/emit_mcp*.py, src/openapi_client/mcp.py]: Consolidate OpenAPI-to-MCP tool schema extraction into extract_mcp_tools_from_spec; delegate tools/list, resources/list, and resources/read in cli.py:run_mcp_server to openapi_client.mcp; use http.server.HTTPServer in serve_json_rpc. - [src/openapi_client/cli.py]: Implement --truth filtering in sync_dir; pass args.mcp to generate_from_openapi; merge AST changes on top of openapi.snapshot.json in generate_to_openapi. - [src/openapi_client/tests/{emit,parse}.py]: Replace hardcoded Petstore/Chaos-audit literals with schema-driven example/default/enum/format resolution and response status code inspection; implement SSE response extraction in ASTTestExtractor; implement parse_cli_sdk_cdd and parse_sqlalchemy_cdd. - [src/**, scripts/**]: Enforce from __future__ import annotations, complete PEP 484 parameter/return type annotations, remove all # type: ignore suppressions and dead Pydantic v1 fallbacks, and extend scripts/doc_coverage.py to verify argument docstrings. - [tests/*]: Isolate all filesystem tests via tmp_path/monkeypatch, replace tautological/empty pass tests with behavioral assertions, and consolidate 41 fragmented test files into 12 domain-aligned test modules achieving 100.00% line/branch and docstring coverage.
1 parent 8d38bb4 commit cbb8208

113 files changed

Lines changed: 6594 additions & 9534 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.coveragerc‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
[run]
2-
omit =
3-
src/client.py
4-
src/mock_server.py
5-
update_models.py
6-
update_tests.py
7-
update_tests2.py
8-
update_tests3.py
9-
update_tests4.py
2+
branch = True
3+
source =
4+
src/openapi_client
5+
scripts

‎.gitignore‎

Lines changed: 33 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -3,54 +3,14 @@ out_server/
33
build/
44
dist/
55
*.xml
6-
.vscode
7-
.idea
6+
.vscode/
7+
.idea/
88
*venv*
9-
__pycache__
10-
*.pyc
11-
coverage.json
12-
3.2.0.md
13-
.coverage
14-
/test_client.py
15-
openapi.json
16-
17-
client.py
18-
docs.json
19-
spec.json
20-
all_objects.txt
21-
objects.txt
22-
compliance-openapi-3-2-0/
23-
debug_compliance.py
24-
fix_all.py
25-
fix_coverage.py
26-
test_get_terms.py
27-
update.py
28-
29-
*.wasm
30-
node_modules/
319
__pycache__/
32-
*.o
33-
*.so
34-
target/
35-
.idea/
36-
temp-*
37-
temp-py/
38-
.git.bak
39-
test_wasi.js
40-
41-
# ---------------------------------------------------
42-
# Additional comprehensive exclusions for careless git add .
43-
# ---------------------------------------------------
44-
45-
# Byte-compiled / optimized / DLL files
4610
*.py[cod]
4711
*$py.class
4812
*.pyd
49-
50-
# C extensions
5113
*.so
52-
53-
# Distribution / packaging
5414
.Python
5515
env/
5616
venv/
@@ -59,55 +19,31 @@ env.bak/
5919
venv.bak/
6020
.env
6121
.venv
62-
63-
# Unit test / coverage reports
6422
htmlcov/
6523
.tox/
6624
.nox/
6725
.coverage
6826
.coverage.*
69-
.cache
70-
nosetests.xml
27+
coverage.json
7128
coverage.xml
7229
*.cover
7330
*.sfdx
7431
.hypothesis/
7532
.pytest_cache/
76-
77-
# Jupyter Notebook
33+
.cache/
34+
nosetests.xml
7835
.ipynb_checkpoints
79-
80-
# Environments
81-
.env
82-
.venv
83-
env/
84-
venv/
85-
ENV/
86-
env.bak/
87-
venv.bak/
88-
89-
# mypy
9036
.mypy_cache/
9137
.dmypy.json
9238
dmypy.json
9339
.pyre/
94-
95-
# OS generated files
9640
.DS_Store
9741
.DS_Store?
9842
._*
9943
.Spotlight-V100
10044
.Trashes
10145
ehthumbs.db
10246
Thumbs.db
103-
104-
# ---------------------------------------------------
105-
# Further comprehensive wildcards for safe `git add .`
106-
# ---------------------------------------------------
107-
108-
# IDEs and Editors
109-
.vscode/
110-
.idea/
11147
*.swp
11248
*~
11349
*.bak
@@ -119,13 +55,9 @@ Thumbs.db
11955
.cproject
12056
.pydevproject
12157
.settings/
122-
123-
# Logs and databases
12458
*.log
12559
*.sqlite
12660
*.sqlite3
127-
128-
# Build and Packaging artifacts
12961
*.egg-info/
13062
.eggs/
13163
*.egg
@@ -136,8 +68,6 @@ Thumbs.db
13668
*.zip
13769
MANIFEST
13870
out/
139-
140-
# Caches and tools
14171
.ruff_cache/
14272
.uv_cache/
14373
.pre-commit-config.yaml.bak
@@ -147,42 +77,45 @@ celerybeat.pid
14777
*.pid
14878
*.seed
14979
*.pid.lock
150-
151-
# Pyenv / Misc
15280
.python-version
15381
*.snapshot.json
154-
155-
# Profiling and Patching
15682
*.patch
15783
*.diff
15884
*.pyprof
15985
*.lprof
16086
*.pstats
16187
*.cprof
162-
163-
# ---------------------------------------------------
164-
# Custom Scratch and Temporary Files
165-
# ---------------------------------------------------
88+
*.wasm
89+
node_modules/
90+
*.o
91+
target/
92+
temp-*
93+
temp-py/
94+
.git.bak
95+
test_wasi.js
96+
bin/
97+
*.lock
98+
prompt*.md
99+
.tmp_*/
100+
/3.2.0.md
101+
/test_client.py
102+
/openapi.json
103+
/client.py
104+
/src/client.py
105+
/src/mock_server.py
106+
/docs.json
107+
/spec.json
108+
/all_objects.txt
109+
/objects.txt
110+
/compliance-openapi-3-2-0/
111+
/debug_compliance.py
112+
/fix_all.py
113+
/fix_coverage.py
114+
/test_get_terms.py
115+
/update.py
166116
/tmp.txt
167117
/tmp*.txt
168118
/update_models.py
169119
/update_tests*.py
170120
/fix_*.py
171-
172121
/test_pydantic_v1.py
173-
174-
# Pre-commit test directories
175-
.tmp_swagger/
176-
.tmp_oas3/
177-
.pytest_cache
178-
bin
179-
180-
# Transient and Lock Files
181-
*.lock
182-
prompt*.md
183-
update_models.py
184-
update_tests.py
185-
update_tests2.py
186-
update_tests3.py
187-
update_tests4.py
188-
.tmp_*

‎run_manual_test.py‎

Lines changed: 0 additions & 21 deletions
This file was deleted.

‎scripts/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
"""Scripts package."""
2+
3+
from __future__ import annotations

‎scripts/build_wasm.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
"""Build script for WebAssembly bundle."""
22

3+
from __future__ import annotations
4+
35
import os
46
import subprocess
7+
import zipfile
58

69

7-
def main():
10+
def main() -> None:
811
"""Build WASI output or fallback to zip bundle."""
912
os.makedirs("bin", exist_ok=True)
1013
wasm_out = os.path.join("bin", "cdd-python-all.wasm")
@@ -20,11 +23,8 @@ def main():
2023
if os.path.exists(wasm_out):
2124
os.remove(wasm_out)
2225

23-
# Cross-platform zip
24-
import zipfile
25-
2626
with zipfile.ZipFile(wasm_out, "w", zipfile.ZIP_DEFLATED) as zipf:
27-
for root, dirs, files in os.walk("src"):
27+
for root, _dirs, files in os.walk("src"):
2828
for file in files:
2929
if "__pycache__" not in root and file.endswith(".py"):
3030
file_path = os.path.join(root, file)

‎scripts/doc_coverage.py‎

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1-
"""Module for calculating docstring coverage."""
1+
"""Module for calculating docstring coverage across modules, classes, functions, and arguments."""
2+
3+
from __future__ import annotations
24

35
import ast
46
import os
57

68

7-
def calculate_doc_coverage(directory):
8-
"""Calculate the percentage of functions, classes, and modules with docstrings."""
9+
def calculate_doc_coverage(directory: str) -> tuple[float, int, int]:
10+
"""Calculate the percentage of modules, classes, functions, and arguments with docstrings.
11+
12+
Args:
13+
directory: Root directory to scan recursively for `.py` files.
14+
15+
Returns:
16+
A tuple of `(coverage_percentage, documented_count, total_count)`.
17+
"""
918
total = 0
1019
with_docs = 0
1120

@@ -18,10 +27,9 @@ def calculate_doc_coverage(directory):
1827
content = f.read()
1928
try:
2029
tree = ast.parse(content)
21-
except Exception:
30+
except SyntaxError:
2231
continue
2332

24-
# Check module docstring
2533
total += 1
2634
if ast.get_docstring(tree):
2735
with_docs += 1
@@ -32,18 +40,28 @@ def calculate_doc_coverage(directory):
3240
if isinstance(
3341
node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
3442
):
35-
# skip private methods but keep __init__
3643
if node.name.startswith("__") and node.name != "__init__":
3744
continue
38-
# Actually let's just ignore __init__ or count it if the class has a docstring?
39-
# Let's count __init__ as requiring a docstring.
4045
total += 1
41-
if ast.get_docstring(node):
46+
doc = ast.get_docstring(node)
47+
if doc:
4248
with_docs += 1
4349
else:
4450
print(f"Missing docstring: {path} :: {node.name}")
4551

46-
percent = (with_docs / total * 100) if total > 0 else 0
52+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
53+
for arg in node.args.args:
54+
if arg.arg in ("self", "cls"):
55+
continue
56+
total += 1
57+
if doc and arg.arg in doc:
58+
with_docs += 1
59+
else:
60+
print(
61+
f"Missing arg docstring: {path} :: {node.name}({arg.arg})"
62+
)
63+
64+
percent = (with_docs / total * 100) if total > 0 else 0.0
4765
return percent, with_docs, total
4866

4967

0 commit comments

Comments
 (0)