pyopenapi-gen 0.21.1__py3-none-any.whl → 0.22.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyopenapi-gen might be problematic. Click here for more details.
- pyopenapi_gen/__init__.py +1 -1
- pyopenapi_gen/emitters/endpoints_emitter.py +18 -3
- pyopenapi_gen/emitters/mocks_emitter.py +185 -0
- pyopenapi_gen/generator/client_generator.py +15 -0
- pyopenapi_gen/visit/client_visitor.py +253 -2
- pyopenapi_gen/visit/endpoint/endpoint_visitor.py +209 -1
- pyopenapi_gen/visit/endpoint/generators/mock_generator.py +140 -0
- pyopenapi_gen-0.22.0.dist-info/METADATA +1139 -0
- {pyopenapi_gen-0.21.1.dist-info → pyopenapi_gen-0.22.0.dist-info}/RECORD +12 -10
- pyopenapi_gen-0.21.1.dist-info/METADATA +0 -645
- {pyopenapi_gen-0.21.1.dist-info → pyopenapi_gen-0.22.0.dist-info}/WHEEL +0 -0
- {pyopenapi_gen-0.21.1.dist-info → pyopenapi_gen-0.22.0.dist-info}/entry_points.txt +0 -0
- {pyopenapi_gen-0.21.1.dist-info → pyopenapi_gen-0.22.0.dist-info}/licenses/LICENSE +0 -0
pyopenapi_gen/__init__.py
CHANGED
|
@@ -50,7 +50,7 @@ __all__ = [
|
|
|
50
50
|
]
|
|
51
51
|
|
|
52
52
|
# Semantic version of the generator core – automatically managed by semantic-release.
|
|
53
|
-
__version__: str = "0.
|
|
53
|
+
__version__: str = "0.22.0"
|
|
54
54
|
|
|
55
55
|
# ---------------------------------------------------------------------------
|
|
56
56
|
# Lazy-loading and autocompletion support (This part remains)
|
|
@@ -197,6 +197,7 @@ class EndpointsEmitter:
|
|
|
197
197
|
canonical_tag_name = tag_map[key]
|
|
198
198
|
module_name = NameSanitizer.sanitize_module_name(canonical_tag_name)
|
|
199
199
|
class_name = NameSanitizer.sanitize_class_name(canonical_tag_name) + "Client"
|
|
200
|
+
protocol_name = f"{class_name}Protocol"
|
|
200
201
|
file_path = endpoints_dir / f"{module_name}.py"
|
|
201
202
|
|
|
202
203
|
# This will set current_file and reset+reinit import_collector's context
|
|
@@ -208,21 +209,35 @@ class EndpointsEmitter:
|
|
|
208
209
|
if self.visitor is None:
|
|
209
210
|
raise RuntimeError("EndpointVisitor not initialized")
|
|
210
211
|
methods = [self.visitor.visit(op, self.context) for op in ops_for_tag]
|
|
211
|
-
|
|
212
|
+
# Pass operations to emit_endpoint_client_class for Protocol generation
|
|
213
|
+
class_content = self.visitor.emit_endpoint_client_class(
|
|
214
|
+
canonical_tag_name, methods, self.context, operations=ops_for_tag
|
|
215
|
+
)
|
|
212
216
|
|
|
213
217
|
imports = self.context.render_imports()
|
|
214
218
|
file_content = imports + "\n\n" + class_content
|
|
215
219
|
self.context.file_manager.write_file(str(file_path), file_content)
|
|
220
|
+
# Store both class and protocol for __init__.py generation
|
|
216
221
|
client_classes.append((class_name, module_name))
|
|
217
222
|
generated_files.append(str(file_path))
|
|
218
223
|
|
|
219
224
|
unique_clients = _deduplicate_tag_clients(client_classes)
|
|
220
225
|
init_lines = []
|
|
221
226
|
if unique_clients:
|
|
222
|
-
|
|
227
|
+
# Export both implementation classes and Protocol classes
|
|
228
|
+
all_list_items = []
|
|
229
|
+
for cls, _ in unique_clients:
|
|
230
|
+
protocol_name = f"{cls}Protocol"
|
|
231
|
+
all_list_items.append(f'"{cls}"')
|
|
232
|
+
all_list_items.append(f'"{protocol_name}"')
|
|
233
|
+
|
|
234
|
+
all_list_items = sorted(all_list_items)
|
|
223
235
|
init_lines.append(f"__all__ = [{', '.join(all_list_items)}]")
|
|
236
|
+
|
|
237
|
+
# Import both implementation and Protocol from each module
|
|
224
238
|
for cls, mod in sorted(unique_clients):
|
|
225
|
-
|
|
239
|
+
protocol_name = f"{cls}Protocol"
|
|
240
|
+
init_lines.append(f"from .{mod} import {cls}, {protocol_name}")
|
|
226
241
|
|
|
227
242
|
endpoints_init_path = endpoints_dir / "__init__.py"
|
|
228
243
|
self.context.file_manager.write_file(str(endpoints_init_path), "\n".join(init_lines) + "\n")
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Emitter for generating mock helper classes.
|
|
3
|
+
|
|
4
|
+
This module creates the mocks/ directory structure with mock implementations
|
|
5
|
+
for both tag-based endpoint clients and the main API client.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import tempfile
|
|
9
|
+
import traceback
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from pyopenapi_gen import IROperation, IRSpec
|
|
14
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
|
15
|
+
from pyopenapi_gen.core.utils import NameSanitizer
|
|
16
|
+
|
|
17
|
+
from ..visit.client_visitor import ClientVisitor
|
|
18
|
+
from ..visit.endpoint.endpoint_visitor import EndpointVisitor
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MocksEmitter:
|
|
22
|
+
"""Generates mock helper classes for testing."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, context: RenderContext) -> None:
|
|
25
|
+
self.endpoint_visitor = EndpointVisitor()
|
|
26
|
+
self.client_visitor = ClientVisitor()
|
|
27
|
+
self.context = context
|
|
28
|
+
|
|
29
|
+
def emit(self, spec: IRSpec, output_dir_str: str) -> list[str]:
|
|
30
|
+
"""
|
|
31
|
+
Generate all mock files in mocks/ directory structure.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
spec: IR specification
|
|
35
|
+
output_dir_str: Output directory path
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
List of generated file paths
|
|
39
|
+
"""
|
|
40
|
+
error_log = Path(tempfile.gettempdir()) / "pyopenapi_gen_mocks_error.log"
|
|
41
|
+
generated_files = []
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
output_dir_abs = Path(output_dir_str)
|
|
45
|
+
mocks_dir = output_dir_abs / "mocks"
|
|
46
|
+
mocks_dir.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
|
|
48
|
+
# Group operations by tag
|
|
49
|
+
operations_by_tag = self._group_operations_by_tag(spec)
|
|
50
|
+
|
|
51
|
+
# Track tag information for main client generation
|
|
52
|
+
tag_tuples = []
|
|
53
|
+
|
|
54
|
+
# Generate mock endpoint classes
|
|
55
|
+
mock_endpoints_dir = mocks_dir / "endpoints"
|
|
56
|
+
mock_endpoints_dir.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
|
|
58
|
+
for tag, ops_for_tag in operations_by_tag.items():
|
|
59
|
+
if not ops_for_tag:
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
canonical_tag_name = tag if tag else "default"
|
|
63
|
+
class_name = NameSanitizer.sanitize_class_name(canonical_tag_name) + "Client"
|
|
64
|
+
module_name = NameSanitizer.sanitize_module_name(canonical_tag_name)
|
|
65
|
+
|
|
66
|
+
# Track for main client generation
|
|
67
|
+
tag_tuples.append((canonical_tag_name, class_name, module_name))
|
|
68
|
+
|
|
69
|
+
# Generate mock class
|
|
70
|
+
mock_file_path = mock_endpoints_dir / f"mock_{module_name}.py"
|
|
71
|
+
self.context.set_current_file(str(mock_file_path))
|
|
72
|
+
|
|
73
|
+
mock_code = self.endpoint_visitor.generate_endpoint_mock_class(
|
|
74
|
+
canonical_tag_name, ops_for_tag, self.context
|
|
75
|
+
)
|
|
76
|
+
imports_code = self.context.render_imports()
|
|
77
|
+
file_content = imports_code + "\n\n" + mock_code
|
|
78
|
+
|
|
79
|
+
self.context.file_manager.write_file(str(mock_file_path), file_content)
|
|
80
|
+
generated_files.append(str(mock_file_path))
|
|
81
|
+
|
|
82
|
+
# Generate mock endpoints __init__.py
|
|
83
|
+
endpoints_init_path = mock_endpoints_dir / "__init__.py"
|
|
84
|
+
endpoints_init_content = self._generate_mock_endpoints_init(tag_tuples)
|
|
85
|
+
self.context.file_manager.write_file(str(endpoints_init_path), endpoints_init_content)
|
|
86
|
+
generated_files.append(str(endpoints_init_path))
|
|
87
|
+
|
|
88
|
+
# Generate main mock client
|
|
89
|
+
mock_client_path = mocks_dir / "mock_client.py"
|
|
90
|
+
self.context.set_current_file(str(mock_client_path))
|
|
91
|
+
|
|
92
|
+
mock_client_code = self.client_visitor.generate_client_mock_class(spec, self.context, tag_tuples)
|
|
93
|
+
imports_code = self.context.render_imports()
|
|
94
|
+
file_content = imports_code + "\n\n" + mock_client_code
|
|
95
|
+
|
|
96
|
+
self.context.file_manager.write_file(str(mock_client_path), file_content)
|
|
97
|
+
generated_files.append(str(mock_client_path))
|
|
98
|
+
|
|
99
|
+
# Generate mocks __init__.py
|
|
100
|
+
mocks_init_path = mocks_dir / "__init__.py"
|
|
101
|
+
mocks_init_content = self._generate_mocks_init(tag_tuples)
|
|
102
|
+
self.context.file_manager.write_file(str(mocks_init_path), mocks_init_content)
|
|
103
|
+
generated_files.append(str(mocks_init_path))
|
|
104
|
+
|
|
105
|
+
return generated_files
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
with open(error_log, "a") as f:
|
|
109
|
+
f.write(f"ERROR in MocksEmitter.emit: {e}\n")
|
|
110
|
+
f.write(traceback.format_exc())
|
|
111
|
+
raise
|
|
112
|
+
|
|
113
|
+
def _group_operations_by_tag(self, spec: IRSpec) -> dict[str, list[IROperation]]:
|
|
114
|
+
"""Group operations by their OpenAPI tag."""
|
|
115
|
+
operations_by_tag: dict[str, list[IROperation]] = defaultdict(list)
|
|
116
|
+
|
|
117
|
+
for operation in spec.operations:
|
|
118
|
+
tag = operation.tags[0] if operation.tags else "default"
|
|
119
|
+
operations_by_tag[tag].append(operation)
|
|
120
|
+
|
|
121
|
+
return operations_by_tag
|
|
122
|
+
|
|
123
|
+
def _generate_mock_endpoints_init(self, tag_tuples: list[tuple[str, str, str]]) -> str:
|
|
124
|
+
"""Generate __init__.py for mocks/endpoints/ directory."""
|
|
125
|
+
lines = []
|
|
126
|
+
lines.append('"""')
|
|
127
|
+
lines.append("Mock endpoint clients for testing.")
|
|
128
|
+
lines.append("")
|
|
129
|
+
lines.append("Import mock classes to use as base classes for your test doubles.")
|
|
130
|
+
lines.append('"""')
|
|
131
|
+
lines.append("")
|
|
132
|
+
|
|
133
|
+
# Import statements
|
|
134
|
+
all_exports = []
|
|
135
|
+
for tag, class_name, module_name in sorted(tag_tuples, key=lambda x: x[2]):
|
|
136
|
+
mock_class_name = f"Mock{class_name}"
|
|
137
|
+
lines.append(f"from .mock_{module_name} import {mock_class_name}")
|
|
138
|
+
all_exports.append(mock_class_name)
|
|
139
|
+
|
|
140
|
+
lines.append("")
|
|
141
|
+
lines.append("__all__ = [")
|
|
142
|
+
for export in all_exports:
|
|
143
|
+
lines.append(f' "{export}",')
|
|
144
|
+
lines.append("]")
|
|
145
|
+
|
|
146
|
+
return "\n".join(lines)
|
|
147
|
+
|
|
148
|
+
def _generate_mocks_init(self, tag_tuples: list[tuple[str, str, str]]) -> str:
|
|
149
|
+
"""Generate __init__.py for mocks/ directory."""
|
|
150
|
+
lines = []
|
|
151
|
+
lines.append('"""')
|
|
152
|
+
lines.append("Mock implementations for testing.")
|
|
153
|
+
lines.append("")
|
|
154
|
+
lines.append("These mocks implement the Protocol contracts without requiring")
|
|
155
|
+
lines.append("network transport or authentication. Use them as base classes")
|
|
156
|
+
lines.append("in your tests.")
|
|
157
|
+
lines.append("")
|
|
158
|
+
lines.append("Example:")
|
|
159
|
+
lines.append(" from myapi.mocks import MockAPIClient, MockPetsClient")
|
|
160
|
+
lines.append("")
|
|
161
|
+
lines.append(" class TestPetsClient(MockPetsClient):")
|
|
162
|
+
lines.append(" async def list_pets(self, limit: int | None = None) -> list[Pet]:")
|
|
163
|
+
lines.append(" return [Pet(id=1, name='Test Pet')]")
|
|
164
|
+
lines.append("")
|
|
165
|
+
lines.append(" client = MockAPIClient(pets=TestPetsClient())")
|
|
166
|
+
lines.append('"""')
|
|
167
|
+
lines.append("")
|
|
168
|
+
|
|
169
|
+
# Import main mock client
|
|
170
|
+
lines.append("from .mock_client import MockAPIClient")
|
|
171
|
+
|
|
172
|
+
# Import mock endpoint classes
|
|
173
|
+
all_exports = ["MockAPIClient"]
|
|
174
|
+
for tag, class_name, module_name in sorted(tag_tuples, key=lambda x: x[2]):
|
|
175
|
+
mock_class_name = f"Mock{class_name}"
|
|
176
|
+
lines.append(f"from .endpoints.mock_{module_name} import {mock_class_name}")
|
|
177
|
+
all_exports.append(mock_class_name)
|
|
178
|
+
|
|
179
|
+
lines.append("")
|
|
180
|
+
lines.append("__all__ = [")
|
|
181
|
+
for export in all_exports:
|
|
182
|
+
lines.append(f' "{export}",')
|
|
183
|
+
lines.append("]")
|
|
184
|
+
|
|
185
|
+
return "\n".join(lines)
|
|
@@ -19,6 +19,7 @@ from pyopenapi_gen.emitters.client_emitter import ClientEmitter
|
|
|
19
19
|
from pyopenapi_gen.emitters.core_emitter import CoreEmitter
|
|
20
20
|
from pyopenapi_gen.emitters.endpoints_emitter import EndpointsEmitter
|
|
21
21
|
from pyopenapi_gen.emitters.exceptions_emitter import ExceptionsEmitter
|
|
22
|
+
from pyopenapi_gen.emitters.mocks_emitter import MocksEmitter
|
|
22
23
|
from pyopenapi_gen.emitters.models_emitter import ModelsEmitter
|
|
23
24
|
|
|
24
25
|
logger = logging.getLogger(__name__)
|
|
@@ -277,6 +278,13 @@ class ClientGenerator:
|
|
|
277
278
|
temp_generated_files += client_files
|
|
278
279
|
self._log_progress(f"Generated {len(client_files)} client files (temp)", "EMIT_CLIENT_TEMP")
|
|
279
280
|
|
|
281
|
+
# 7. MocksEmitter (emits mock files to tmp_out_dir_for_diff)
|
|
282
|
+
self._log_progress("Generating mock helper classes (temp)", "EMIT_MOCKS_TEMP")
|
|
283
|
+
mocks_emitter = MocksEmitter(context=tmp_render_context_for_diff)
|
|
284
|
+
mock_files = [Path(p) for p in mocks_emitter.emit(ir, str(tmp_out_dir_for_diff))]
|
|
285
|
+
temp_generated_files += mock_files
|
|
286
|
+
self._log_progress(f"Generated {len(mock_files)} mock files (temp)", "EMIT_MOCKS_TEMP")
|
|
287
|
+
|
|
280
288
|
# Post-processing should run on the temporary files if enabled
|
|
281
289
|
if not no_postprocess:
|
|
282
290
|
self._log_progress("Running post-processing on temporary files", "POSTPROCESS_TEMP")
|
|
@@ -431,6 +439,13 @@ class ClientGenerator:
|
|
|
431
439
|
generated_files += client_files
|
|
432
440
|
self._log_progress(f"Generated {len(client_files)} client files", "EMIT_CLIENT")
|
|
433
441
|
|
|
442
|
+
# 7. MocksEmitter
|
|
443
|
+
self._log_progress("Generating mock helper classes", "EMIT_MOCKS")
|
|
444
|
+
mocks_emitter = MocksEmitter(context=main_render_context)
|
|
445
|
+
mock_files = [Path(p) for p in mocks_emitter.emit(ir, str(out_dir))]
|
|
446
|
+
generated_files += mock_files
|
|
447
|
+
self._log_progress(f"Generated {len(mock_files)} mock files", "EMIT_MOCKS")
|
|
448
|
+
|
|
434
449
|
# After all emitters, if core_package is specified (external core),
|
|
435
450
|
# create a rich __init__.py in the client's output_package (out_dir).
|
|
436
451
|
if core_package: # core_package is the user-provided original arg
|
|
@@ -24,6 +24,7 @@ class ClientVisitor:
|
|
|
24
24
|
pass
|
|
25
25
|
|
|
26
26
|
def visit(self, spec: IRSpec, context: RenderContext) -> str:
|
|
27
|
+
# Step 1: Process tags and build tag_tuples
|
|
27
28
|
tag_candidates: dict[str, list[str]] = {}
|
|
28
29
|
for op in spec.operations:
|
|
29
30
|
# Use DEFAULT_TAG consistent with EndpointsEmitter
|
|
@@ -60,11 +61,38 @@ class ClientVisitor:
|
|
|
60
61
|
)
|
|
61
62
|
for key in sorted(tag_map)
|
|
62
63
|
]
|
|
64
|
+
|
|
65
|
+
# Step 2: Generate Protocol definition
|
|
66
|
+
protocol_code = self.generate_client_protocol(spec, context, tag_tuples)
|
|
67
|
+
|
|
68
|
+
# Step 3: Generate implementation class
|
|
69
|
+
impl_code = self._generate_client_implementation(spec, context, tag_tuples)
|
|
70
|
+
|
|
71
|
+
# Step 4: Combine Protocol and implementation
|
|
72
|
+
return f"{protocol_code}\n\n\n{impl_code}"
|
|
73
|
+
|
|
74
|
+
def _generate_client_implementation(
|
|
75
|
+
self, spec: IRSpec, context: RenderContext, tag_tuples: list[tuple[str, str, str]]
|
|
76
|
+
) -> str:
|
|
77
|
+
"""
|
|
78
|
+
Generate the APIClient implementation class.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
spec: The IR specification
|
|
82
|
+
context: Render context for import management
|
|
83
|
+
tag_tuples: List of (tag_name, class_name, module_name) tuples
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Implementation class code as string
|
|
87
|
+
"""
|
|
63
88
|
writer = CodeWriter()
|
|
64
89
|
# Register all endpoint client imports using relative imports (endpoints are within the same package)
|
|
65
90
|
for _, class_name, module_name in tag_tuples:
|
|
66
91
|
# Use relative imports for endpoints since they're part of the same generated package
|
|
92
|
+
# Import both the implementation and the Protocol
|
|
93
|
+
protocol_name = f"{class_name}Protocol"
|
|
67
94
|
context.import_collector.add_relative_import(f".endpoints.{module_name}", class_name)
|
|
95
|
+
context.import_collector.add_relative_import(f".endpoints.{module_name}", protocol_name)
|
|
68
96
|
|
|
69
97
|
# Register core/config/typing imports for class signature
|
|
70
98
|
# Use LOGICAL import path for core components
|
|
@@ -82,8 +110,8 @@ class ClientVisitor:
|
|
|
82
110
|
context.add_typing_imports_for_type("HttpTransport | None")
|
|
83
111
|
context.add_typing_imports_for_type("Any")
|
|
84
112
|
context.add_typing_imports_for_type("Dict")
|
|
85
|
-
# Class definition
|
|
86
|
-
writer.write_line("class APIClient:")
|
|
113
|
+
# Class definition - implements Protocol
|
|
114
|
+
writer.write_line("class APIClient(APIClientProtocol):")
|
|
87
115
|
writer.indent()
|
|
88
116
|
# Build docstring for APIClient
|
|
89
117
|
docstring_lines = []
|
|
@@ -224,3 +252,226 @@ class ClientVisitor:
|
|
|
224
252
|
context.add_import(f"{context.core_package_name}.config", "ClientConfig")
|
|
225
253
|
|
|
226
254
|
return writer.get_code()
|
|
255
|
+
|
|
256
|
+
def generate_client_protocol(
|
|
257
|
+
self, spec: IRSpec, context: RenderContext, tag_tuples: list[tuple[str, str, str]]
|
|
258
|
+
) -> str:
|
|
259
|
+
"""
|
|
260
|
+
Generate APIClientProtocol defining the client interface.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
spec: The IR specification
|
|
264
|
+
context: Render context for import management
|
|
265
|
+
tag_tuples: List of (tag_name, class_name, module_name) tuples
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Protocol class code as string with:
|
|
269
|
+
- Tag-based endpoint properties
|
|
270
|
+
- Standard methods (request, close, __aenter__, __aexit__)
|
|
271
|
+
"""
|
|
272
|
+
# Register Protocol imports
|
|
273
|
+
context.add_typing_imports_for_type("Protocol")
|
|
274
|
+
context.add_import("typing", "runtime_checkable")
|
|
275
|
+
context.add_typing_imports_for_type("Any")
|
|
276
|
+
|
|
277
|
+
writer = CodeWriter()
|
|
278
|
+
|
|
279
|
+
# Protocol class header
|
|
280
|
+
writer.write_line("@runtime_checkable")
|
|
281
|
+
writer.write_line("class APIClientProtocol(Protocol):")
|
|
282
|
+
writer.indent()
|
|
283
|
+
|
|
284
|
+
# Docstring
|
|
285
|
+
writer.write_line('"""Protocol defining the interface of APIClient for dependency injection."""')
|
|
286
|
+
writer.write_line("")
|
|
287
|
+
|
|
288
|
+
# Tag-based endpoint properties
|
|
289
|
+
for tag, class_name, module_name in tag_tuples:
|
|
290
|
+
# Use forward reference for tag client protocol
|
|
291
|
+
protocol_name = f"{class_name}Protocol"
|
|
292
|
+
writer.write_line("@property")
|
|
293
|
+
writer.write_line(f"def {module_name}(self) -> '{protocol_name}':")
|
|
294
|
+
writer.indent()
|
|
295
|
+
writer.write_line("...")
|
|
296
|
+
writer.dedent()
|
|
297
|
+
writer.write_line("")
|
|
298
|
+
|
|
299
|
+
# Standard methods
|
|
300
|
+
# request method
|
|
301
|
+
writer.write_line("async def request(self, method: str, url: str, **kwargs: Any) -> Any:")
|
|
302
|
+
writer.indent()
|
|
303
|
+
writer.write_line("...")
|
|
304
|
+
writer.dedent()
|
|
305
|
+
writer.write_line("")
|
|
306
|
+
|
|
307
|
+
# close method
|
|
308
|
+
writer.write_line("async def close(self) -> None:")
|
|
309
|
+
writer.indent()
|
|
310
|
+
writer.write_line("...")
|
|
311
|
+
writer.dedent()
|
|
312
|
+
writer.write_line("")
|
|
313
|
+
|
|
314
|
+
# __aenter__ method
|
|
315
|
+
writer.write_line("async def __aenter__(self) -> 'APIClientProtocol':")
|
|
316
|
+
writer.indent()
|
|
317
|
+
writer.write_line("...")
|
|
318
|
+
writer.dedent()
|
|
319
|
+
writer.write_line("")
|
|
320
|
+
|
|
321
|
+
# __aexit__ method
|
|
322
|
+
context.add_typing_imports_for_type("type[BaseException] | None")
|
|
323
|
+
context.add_typing_imports_for_type("BaseException | None")
|
|
324
|
+
context.add_typing_imports_for_type("object | None")
|
|
325
|
+
writer.write_line(
|
|
326
|
+
"async def __aexit__(self, exc_type: type[BaseException] | None, "
|
|
327
|
+
"exc_val: BaseException | None, exc_tb: object | None) -> None:"
|
|
328
|
+
)
|
|
329
|
+
writer.indent()
|
|
330
|
+
writer.write_line("...")
|
|
331
|
+
writer.dedent()
|
|
332
|
+
|
|
333
|
+
writer.dedent() # Close class
|
|
334
|
+
return writer.get_code()
|
|
335
|
+
|
|
336
|
+
def generate_client_mock_class(
|
|
337
|
+
self, spec: IRSpec, context: RenderContext, tag_tuples: list[tuple[str, str, str]]
|
|
338
|
+
) -> str:
|
|
339
|
+
"""
|
|
340
|
+
Generate MockAPIClient for testing.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
spec: The IR specification
|
|
344
|
+
context: Render context for import management
|
|
345
|
+
tag_tuples: List of (tag, class_name, module_name) tuples
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Mock client class code as string
|
|
349
|
+
"""
|
|
350
|
+
# Import TYPE_CHECKING for Protocol imports
|
|
351
|
+
context.add_import("typing", "TYPE_CHECKING")
|
|
352
|
+
context.add_import("typing", "Any")
|
|
353
|
+
|
|
354
|
+
writer = CodeWriter()
|
|
355
|
+
|
|
356
|
+
# TYPE_CHECKING imports
|
|
357
|
+
writer.write_line("if TYPE_CHECKING:")
|
|
358
|
+
writer.indent()
|
|
359
|
+
writer.write_line("from ..client import APIClientProtocol")
|
|
360
|
+
for tag, class_name, module_name in tag_tuples:
|
|
361
|
+
protocol_name = f"{class_name}Protocol"
|
|
362
|
+
writer.write_line(f"from ..endpoints.{module_name} import {protocol_name}")
|
|
363
|
+
writer.dedent()
|
|
364
|
+
writer.write_line("")
|
|
365
|
+
|
|
366
|
+
# Import mock endpoint classes
|
|
367
|
+
for tag, class_name, module_name in tag_tuples:
|
|
368
|
+
mock_class_name = f"Mock{class_name}"
|
|
369
|
+
writer.write_line(f"from .endpoints.mock_{module_name} import {mock_class_name}")
|
|
370
|
+
writer.write_line("")
|
|
371
|
+
|
|
372
|
+
# Class definition
|
|
373
|
+
writer.write_line("class MockAPIClient:")
|
|
374
|
+
writer.indent()
|
|
375
|
+
|
|
376
|
+
# Docstring
|
|
377
|
+
writer.write_line('"""')
|
|
378
|
+
writer.write_line("Mock implementation of APIClient for testing.")
|
|
379
|
+
writer.write_line("")
|
|
380
|
+
writer.write_line("Auto-creates default mock implementations for all tag-based endpoint clients.")
|
|
381
|
+
writer.write_line("You can override specific tag clients by passing them to the constructor.")
|
|
382
|
+
writer.write_line("")
|
|
383
|
+
writer.write_line("Example:")
|
|
384
|
+
writer.write_line(" # Use all defaults")
|
|
385
|
+
writer.write_line(" client = MockAPIClient()")
|
|
386
|
+
writer.write_line("")
|
|
387
|
+
writer.write_line(" # Override specific tag client")
|
|
388
|
+
for tag, class_name, module_name in tag_tuples[:1]: # Show example with first tag
|
|
389
|
+
mock_class_name = f"Mock{class_name}"
|
|
390
|
+
writer.write_line(f" class My{class_name}Mock({mock_class_name}):")
|
|
391
|
+
writer.write_line(" async def method_name(self, ...) -> ReturnType:")
|
|
392
|
+
writer.write_line(" return test_data")
|
|
393
|
+
writer.write_line("")
|
|
394
|
+
writer.write_line(f" client = MockAPIClient({module_name}=My{class_name}Mock())")
|
|
395
|
+
break
|
|
396
|
+
writer.write_line('"""')
|
|
397
|
+
writer.write_line("")
|
|
398
|
+
|
|
399
|
+
# Constructor
|
|
400
|
+
writer.write_line("def __init__(")
|
|
401
|
+
writer.indent()
|
|
402
|
+
writer.write_line("self,")
|
|
403
|
+
for tag, class_name, module_name in tag_tuples:
|
|
404
|
+
protocol_name = f"{class_name}Protocol"
|
|
405
|
+
writer.write_line(f'{module_name}: "{protocol_name} | None" = None,')
|
|
406
|
+
writer.dedent()
|
|
407
|
+
writer.write_line(") -> None:")
|
|
408
|
+
writer.indent()
|
|
409
|
+
|
|
410
|
+
# Initialize tag clients
|
|
411
|
+
for tag, class_name, module_name in tag_tuples:
|
|
412
|
+
mock_class_name = f"Mock{class_name}"
|
|
413
|
+
writer.write_line(
|
|
414
|
+
f"self._{module_name} = {module_name} if {module_name} is not None else {mock_class_name}()"
|
|
415
|
+
)
|
|
416
|
+
writer.dedent()
|
|
417
|
+
writer.write_line("")
|
|
418
|
+
|
|
419
|
+
# Properties for tag clients
|
|
420
|
+
for tag, class_name, module_name in tag_tuples:
|
|
421
|
+
protocol_name = f"{class_name}Protocol"
|
|
422
|
+
writer.write_line("@property")
|
|
423
|
+
writer.write_line(f'def {module_name}(self) -> "{protocol_name}":')
|
|
424
|
+
writer.indent()
|
|
425
|
+
writer.write_line(f"return self._{module_name}")
|
|
426
|
+
writer.dedent()
|
|
427
|
+
writer.write_line("")
|
|
428
|
+
|
|
429
|
+
# request() method
|
|
430
|
+
writer.write_line("async def request(self, method: str, url: str, **kwargs: Any) -> Any:")
|
|
431
|
+
writer.indent()
|
|
432
|
+
writer.write_line('"""')
|
|
433
|
+
writer.write_line("Mock request method - raises NotImplementedError.")
|
|
434
|
+
writer.write_line("")
|
|
435
|
+
writer.write_line("This is a low-level method - consider using tag-specific methods instead.")
|
|
436
|
+
writer.write_line('"""')
|
|
437
|
+
writer.write_line(
|
|
438
|
+
"raise NotImplementedError("
|
|
439
|
+
'"MockAPIClient.request() not implemented. '
|
|
440
|
+
'Use tag-specific methods instead."'
|
|
441
|
+
")"
|
|
442
|
+
)
|
|
443
|
+
writer.dedent()
|
|
444
|
+
writer.write_line("")
|
|
445
|
+
|
|
446
|
+
# close() method
|
|
447
|
+
writer.write_line("async def close(self) -> None:")
|
|
448
|
+
writer.indent()
|
|
449
|
+
writer.write_line('"""Mock close method - no-op for testing."""')
|
|
450
|
+
writer.write_line("pass # No cleanup needed for mocks")
|
|
451
|
+
writer.dedent()
|
|
452
|
+
writer.write_line("")
|
|
453
|
+
|
|
454
|
+
# __aenter__() method
|
|
455
|
+
writer.write_line('async def __aenter__(self) -> "APIClientProtocol":')
|
|
456
|
+
writer.indent()
|
|
457
|
+
writer.write_line('"""Enter async context manager."""')
|
|
458
|
+
writer.write_line("return self")
|
|
459
|
+
writer.dedent()
|
|
460
|
+
writer.write_line("")
|
|
461
|
+
|
|
462
|
+
# __aexit__() method
|
|
463
|
+
writer.write_line(
|
|
464
|
+
"async def __aexit__("
|
|
465
|
+
"self, "
|
|
466
|
+
"exc_type: type[BaseException] | None, "
|
|
467
|
+
"exc_val: BaseException | None, "
|
|
468
|
+
"exc_tb: object | None"
|
|
469
|
+
") -> None:"
|
|
470
|
+
)
|
|
471
|
+
writer.indent()
|
|
472
|
+
writer.write_line('"""Exit async context manager - no-op for mocks."""')
|
|
473
|
+
writer.write_line("pass # No cleanup needed for mocks")
|
|
474
|
+
writer.dedent()
|
|
475
|
+
|
|
476
|
+
writer.dedent() # Close class
|
|
477
|
+
return writer.get_code()
|