pyopenapi-gen 0.21.0__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 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.21.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
- class_content = self.visitor.emit_endpoint_client_class(canonical_tag_name, methods, self.context)
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
- all_list_items = sorted([f'"{cls}"' for cls, _ in unique_clients])
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
- init_lines.append(f"from .{mod} import {cls}")
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()