pyopenapi-gen 0.8.7__py3-none-any.whl → 0.10.1__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.
- pyopenapi_gen/__init__.py +2 -2
- pyopenapi_gen/context/CLAUDE.md +284 -0
- pyopenapi_gen/context/import_collector.py +8 -8
- pyopenapi_gen/core/CLAUDE.md +224 -0
- pyopenapi_gen/core/loader/operations/parser.py +1 -1
- pyopenapi_gen/core/parsing/cycle_helpers.py +1 -1
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +4 -4
- pyopenapi_gen/core/parsing/schema_parser.py +4 -4
- pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +1 -1
- pyopenapi_gen/core/writers/python_construct_renderer.py +2 -2
- pyopenapi_gen/emitters/CLAUDE.md +286 -0
- pyopenapi_gen/emitters/endpoints_emitter.py +1 -1
- pyopenapi_gen/generator/CLAUDE.md +352 -0
- pyopenapi_gen/helpers/CLAUDE.md +325 -0
- pyopenapi_gen/helpers/endpoint_utils.py +2 -2
- pyopenapi_gen/helpers/type_cleaner.py +1 -1
- pyopenapi_gen/helpers/type_resolution/composition_resolver.py +1 -1
- pyopenapi_gen/helpers/type_resolution/finalizer.py +1 -1
- pyopenapi_gen/types/CLAUDE.md +140 -0
- pyopenapi_gen/types/resolvers/schema_resolver.py +2 -2
- pyopenapi_gen/visit/CLAUDE.md +272 -0
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +1 -1
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +1 -1
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +1 -1
- {pyopenapi_gen-0.8.7.dist-info → pyopenapi_gen-0.10.1.dist-info}/METADATA +18 -4
- {pyopenapi_gen-0.8.7.dist-info → pyopenapi_gen-0.10.1.dist-info}/RECORD +29 -22
- {pyopenapi_gen-0.8.7.dist-info → pyopenapi_gen-0.10.1.dist-info}/WHEEL +0 -0
- {pyopenapi_gen-0.8.7.dist-info → pyopenapi_gen-0.10.1.dist-info}/entry_points.txt +0 -0
- {pyopenapi_gen-0.8.7.dist-info → pyopenapi_gen-0.10.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,286 @@
|
|
1
|
+
# emitters/ - File Organization and Output
|
2
|
+
|
3
|
+
## Why This Folder?
|
4
|
+
Transform visitor-generated code strings into properly structured Python packages. Handles file creation, import resolution, and package organization.
|
5
|
+
|
6
|
+
## Key Dependencies
|
7
|
+
- **Input**: Code strings from `../visit/` visitors
|
8
|
+
- **Output**: Python files in target package structure
|
9
|
+
- **Services**: `FileManager` from `../context/file_manager.py`
|
10
|
+
- **Context**: `RenderContext` for import management
|
11
|
+
|
12
|
+
## Essential Architecture
|
13
|
+
|
14
|
+
### 1. Emitter Responsibilities
|
15
|
+
```python
|
16
|
+
# Each emitter handles one aspect of the generated client
|
17
|
+
models_emitter.py → models/ directory with dataclasses/enums
|
18
|
+
endpoints_emitter.py → endpoints/ directory with operation methods
|
19
|
+
client_emitter.py → client.py main interface
|
20
|
+
core_emitter.py → core/ directory with runtime dependencies
|
21
|
+
exceptions_emitter.py → exceptions.py error hierarchy
|
22
|
+
```
|
23
|
+
|
24
|
+
### 2. Package Structure Creation
|
25
|
+
```python
|
26
|
+
# Target structure for generated client
|
27
|
+
output_package/
|
28
|
+
├── __init__.py # Package initialization
|
29
|
+
├── client.py # Main client class
|
30
|
+
├── models/ # Data models
|
31
|
+
│ ├── __init__.py
|
32
|
+
│ ├── user.py
|
33
|
+
│ └── order.py
|
34
|
+
├── endpoints/ # Operation methods
|
35
|
+
│ ├── __init__.py
|
36
|
+
│ ├── users.py
|
37
|
+
│ └── orders.py
|
38
|
+
├── core/ # Runtime dependencies
|
39
|
+
│ ├── __init__.py
|
40
|
+
│ ├── auth/
|
41
|
+
│ ├── exceptions.py
|
42
|
+
│ └── http_transport.py
|
43
|
+
└── exceptions.py # Exception hierarchy
|
44
|
+
```
|
45
|
+
|
46
|
+
## Critical Components
|
47
|
+
|
48
|
+
### models_emitter.py
|
49
|
+
**Purpose**: Create models/ directory with dataclass and enum files
|
50
|
+
```python
|
51
|
+
def emit_models(self, schemas: Dict[str, IRSchema], context: RenderContext) -> None:
|
52
|
+
# 1. Group schemas by module (one file per schema or logical grouping)
|
53
|
+
# 2. Generate code for each schema using ModelVisitor
|
54
|
+
# 3. Create __init__.py with imports
|
55
|
+
# 4. Write files to models/ directory
|
56
|
+
|
57
|
+
for schema_name, schema in schemas.items():
|
58
|
+
module_name = self.get_module_name(schema_name)
|
59
|
+
file_path = self.output_path / "models" / f"{module_name}.py"
|
60
|
+
|
61
|
+
# Generate model code
|
62
|
+
model_code = self.model_visitor.visit_schema(schema, context)
|
63
|
+
|
64
|
+
# Write file
|
65
|
+
self.file_manager.write_file(file_path, model_code)
|
66
|
+
```
|
67
|
+
|
68
|
+
### endpoints_emitter.py
|
69
|
+
**Purpose**: Create endpoints/ directory with operation methods grouped by tag
|
70
|
+
```python
|
71
|
+
def emit_endpoints(self, operations: List[IROperation], context: RenderContext) -> None:
|
72
|
+
# 1. Group operations by OpenAPI tag
|
73
|
+
operations_by_tag = self.group_by_tag(operations)
|
74
|
+
|
75
|
+
# 2. Generate endpoint class for each tag
|
76
|
+
for tag, tag_operations in operations_by_tag.items():
|
77
|
+
class_name = f"{tag.capitalize()}Endpoints"
|
78
|
+
file_path = self.output_path / "endpoints" / f"{tag}.py"
|
79
|
+
|
80
|
+
# Generate endpoint class code
|
81
|
+
endpoint_code = self.endpoint_visitor.visit_tag_operations(tag_operations, context)
|
82
|
+
|
83
|
+
# Write file
|
84
|
+
self.file_manager.write_file(file_path, endpoint_code)
|
85
|
+
```
|
86
|
+
|
87
|
+
### client_emitter.py
|
88
|
+
**Purpose**: Create main client.py with tag-grouped properties
|
89
|
+
```python
|
90
|
+
def emit_client(self, spec: IRSpec, context: RenderContext) -> None:
|
91
|
+
# 1. Generate main client class
|
92
|
+
# 2. Create properties for each tag endpoint
|
93
|
+
# 3. Generate context manager methods
|
94
|
+
# 4. Handle authentication setup
|
95
|
+
|
96
|
+
client_code = self.client_visitor.visit_spec(spec, context)
|
97
|
+
self.file_manager.write_file(self.output_path / "client.py", client_code)
|
98
|
+
```
|
99
|
+
|
100
|
+
### core_emitter.py
|
101
|
+
**Purpose**: Copy runtime dependencies to core/ directory
|
102
|
+
```python
|
103
|
+
def emit_core(self, output_package: str, core_package: str) -> None:
|
104
|
+
# 1. Copy auth/ directory
|
105
|
+
# 2. Copy exceptions.py, http_transport.py, etc.
|
106
|
+
# 3. Update import paths for target package
|
107
|
+
# 4. Handle shared core vs embedded core
|
108
|
+
|
109
|
+
if self.use_shared_core:
|
110
|
+
# Create symlinks or references to shared core
|
111
|
+
pass
|
112
|
+
else:
|
113
|
+
# Copy all core files to client package
|
114
|
+
self.copy_core_files()
|
115
|
+
```
|
116
|
+
|
117
|
+
## File Management Patterns
|
118
|
+
|
119
|
+
### 1. Import Resolution
|
120
|
+
```python
|
121
|
+
# Always resolve imports after code generation
|
122
|
+
def write_file_with_imports(self, file_path: Path, code: str, context: RenderContext) -> None:
|
123
|
+
# 1. Collect imports from context
|
124
|
+
imports = context.get_imports()
|
125
|
+
|
126
|
+
# 2. Sort and deduplicate imports
|
127
|
+
sorted_imports = self.sort_imports(imports)
|
128
|
+
|
129
|
+
# 3. Combine imports with code
|
130
|
+
final_code = self.combine_imports_and_code(sorted_imports, code)
|
131
|
+
|
132
|
+
# 4. Write file
|
133
|
+
self.file_manager.write_file(file_path, final_code)
|
134
|
+
```
|
135
|
+
|
136
|
+
### 2. Package Initialization
|
137
|
+
```python
|
138
|
+
# Always create __init__.py files
|
139
|
+
def create_package_init(self, package_path: Path, exports: List[str]) -> None:
|
140
|
+
init_content = []
|
141
|
+
|
142
|
+
# Add imports for all public exports
|
143
|
+
for export in exports:
|
144
|
+
init_content.append(f"from .{export} import {export}")
|
145
|
+
|
146
|
+
# Add __all__ for explicit exports
|
147
|
+
init_content.append(f"__all__ = {exports}")
|
148
|
+
|
149
|
+
self.file_manager.write_file(package_path / "__init__.py", "\n".join(init_content))
|
150
|
+
```
|
151
|
+
|
152
|
+
### 3. Relative Import Handling
|
153
|
+
```python
|
154
|
+
# Convert absolute imports to relative for generated packages
|
155
|
+
def convert_to_relative_imports(self, code: str, current_package: str) -> str:
|
156
|
+
# Replace absolute imports with relative imports
|
157
|
+
# Example: "from my_client.models.user import User" → "from ..models.user import User"
|
158
|
+
|
159
|
+
import_pattern = re.compile(rf"from {re.escape(current_package)}\.(.+?) import")
|
160
|
+
|
161
|
+
def replace_import(match):
|
162
|
+
import_path = match.group(1)
|
163
|
+
depth = len(import_path.split("."))
|
164
|
+
relative_prefix = "." * depth
|
165
|
+
return f"from {relative_prefix}{import_path} import"
|
166
|
+
|
167
|
+
return import_pattern.sub(replace_import, code)
|
168
|
+
```
|
169
|
+
|
170
|
+
## Dependencies on Other Systems
|
171
|
+
|
172
|
+
### From visit/
|
173
|
+
- Consumes generated code strings
|
174
|
+
- Coordinates with visitors for code generation
|
175
|
+
|
176
|
+
### From context/
|
177
|
+
- `FileManager` for file operations
|
178
|
+
- `RenderContext` for import management
|
179
|
+
- Path resolution utilities
|
180
|
+
|
181
|
+
### From core/
|
182
|
+
- Runtime components copied to generated clients
|
183
|
+
- Template files for package structure
|
184
|
+
|
185
|
+
## Testing Requirements
|
186
|
+
|
187
|
+
### File Creation Tests
|
188
|
+
```python
|
189
|
+
def test_models_emitter__simple_schema__creates_correct_file():
|
190
|
+
# Arrange
|
191
|
+
schema = IRSchema(name="User", type="object", properties={"name": {"type": "string"}})
|
192
|
+
emitter = ModelsEmitter(output_path="/tmp/test")
|
193
|
+
|
194
|
+
# Act
|
195
|
+
emitter.emit_models({"User": schema}, context)
|
196
|
+
|
197
|
+
# Assert
|
198
|
+
assert Path("/tmp/test/models/user.py").exists()
|
199
|
+
content = Path("/tmp/test/models/user.py").read_text()
|
200
|
+
assert "@dataclass" in content
|
201
|
+
assert "name: str" in content
|
202
|
+
```
|
203
|
+
|
204
|
+
### Import Resolution Tests
|
205
|
+
```python
|
206
|
+
def test_emitter__complex_types__resolves_imports_correctly():
|
207
|
+
# Test that imports are correctly collected and written
|
208
|
+
# Verify no duplicate imports
|
209
|
+
# Verify correct import sorting
|
210
|
+
```
|
211
|
+
|
212
|
+
## Extension Points
|
213
|
+
|
214
|
+
### Adding New Emitters
|
215
|
+
```python
|
216
|
+
# Create new emitter for new output aspects
|
217
|
+
class CustomEmitter:
|
218
|
+
def __init__(self, output_path: Path, file_manager: FileManager):
|
219
|
+
self.output_path = output_path
|
220
|
+
self.file_manager = file_manager
|
221
|
+
|
222
|
+
def emit_custom(self, data: Any, context: RenderContext) -> None:
|
223
|
+
# Custom file creation logic
|
224
|
+
pass
|
225
|
+
```
|
226
|
+
|
227
|
+
### Custom Package Structures
|
228
|
+
```python
|
229
|
+
# Modify emitters to create different package layouts
|
230
|
+
class AlternativeModelsEmitter(ModelsEmitter):
|
231
|
+
def get_file_path(self, schema_name: str) -> Path:
|
232
|
+
# Custom file organization logic
|
233
|
+
# Example: Group models by domain
|
234
|
+
domain = self.get_domain(schema_name)
|
235
|
+
return self.output_path / "models" / domain / f"{schema_name.lower()}.py"
|
236
|
+
```
|
237
|
+
|
238
|
+
## Critical Implementation Details
|
239
|
+
|
240
|
+
### File Path Resolution
|
241
|
+
```python
|
242
|
+
# Always use pathlib.Path for cross-platform compatibility
|
243
|
+
def get_output_path(self, package_name: str, module_name: str) -> Path:
|
244
|
+
# Convert package.module to file path
|
245
|
+
parts = package_name.split(".")
|
246
|
+
path = Path(self.project_root)
|
247
|
+
for part in parts:
|
248
|
+
path = path / part
|
249
|
+
return path / f"{module_name}.py"
|
250
|
+
```
|
251
|
+
|
252
|
+
### Error Handling
|
253
|
+
```python
|
254
|
+
def emit_safely(self, generator_func: Callable, context: RenderContext) -> None:
|
255
|
+
try:
|
256
|
+
generator_func(context)
|
257
|
+
except Exception as e:
|
258
|
+
# Add context to file emission errors
|
259
|
+
raise FileEmissionError(f"Failed to emit {self.__class__.__name__}: {e}")
|
260
|
+
```
|
261
|
+
|
262
|
+
### Atomic File Operations
|
263
|
+
```python
|
264
|
+
def write_file_atomically(self, file_path: Path, content: str) -> None:
|
265
|
+
# Write to temporary file first, then move
|
266
|
+
temp_path = file_path.with_suffix(f"{file_path.suffix}.tmp")
|
267
|
+
|
268
|
+
try:
|
269
|
+
temp_path.write_text(content)
|
270
|
+
temp_path.replace(file_path) # Atomic move
|
271
|
+
except Exception:
|
272
|
+
if temp_path.exists():
|
273
|
+
temp_path.unlink()
|
274
|
+
raise
|
275
|
+
```
|
276
|
+
|
277
|
+
### Diff Checking
|
278
|
+
```python
|
279
|
+
def should_write_file(self, file_path: Path, new_content: str) -> bool:
|
280
|
+
# Only write if content changed
|
281
|
+
if not file_path.exists():
|
282
|
+
return True
|
283
|
+
|
284
|
+
existing_content = file_path.read_text()
|
285
|
+
return existing_content != new_content
|
286
|
+
```
|
@@ -211,7 +211,7 @@ class EndpointsEmitter:
|
|
211
211
|
init_lines = []
|
212
212
|
if unique_clients:
|
213
213
|
all_list_items = sorted([f'"{cls}"' for cls, _ in unique_clients])
|
214
|
-
init_lines.append(f"__all__ = [{
|
214
|
+
init_lines.append(f"__all__ = [{', '.join(all_list_items)}]")
|
215
215
|
for cls, mod in sorted(unique_clients):
|
216
216
|
init_lines.append(f"from .{mod} import {cls}")
|
217
217
|
|
@@ -0,0 +1,352 @@
|
|
1
|
+
# generator/ - Main Orchestration
|
2
|
+
|
3
|
+
## Why This Folder?
|
4
|
+
High-level orchestration of the entire code generation pipeline. Coordinates loader → parser → visitors → emitters → post-processing flow.
|
5
|
+
|
6
|
+
## Key Dependencies
|
7
|
+
- **Input**: CLI arguments, OpenAPI spec path
|
8
|
+
- **Output**: Complete generated client package
|
9
|
+
- **Orchestrates**: All other system components
|
10
|
+
- **Error Handling**: `GenerationError` for CLI reporting
|
11
|
+
|
12
|
+
## Essential Architecture
|
13
|
+
|
14
|
+
### 1. Generation Pipeline
|
15
|
+
```python
|
16
|
+
# client_generator.py
|
17
|
+
def generate(self, spec_path: str, project_root: Path, output_package: str,
|
18
|
+
force: bool = False, no_postprocess: bool = False,
|
19
|
+
core_package: str = None) -> None:
|
20
|
+
|
21
|
+
# 1. Load OpenAPI spec
|
22
|
+
spec = self.load_spec(spec_path)
|
23
|
+
|
24
|
+
# 2. Parse to IR
|
25
|
+
ir_spec = self.parse_to_ir(spec)
|
26
|
+
|
27
|
+
# 3. Generate code
|
28
|
+
self.generate_code(ir_spec, project_root, output_package, core_package)
|
29
|
+
|
30
|
+
# 4. Post-process (format, typecheck)
|
31
|
+
if not no_postprocess:
|
32
|
+
self.post_process(project_root, output_package)
|
33
|
+
```
|
34
|
+
|
35
|
+
### 2. Diff Checking
|
36
|
+
```python
|
37
|
+
def generate_with_diff_check(self, ...) -> None:
|
38
|
+
if not force and self.output_exists():
|
39
|
+
# Generate to temporary location
|
40
|
+
temp_output = self.create_temp_output()
|
41
|
+
self.generate_to_path(temp_output)
|
42
|
+
|
43
|
+
# Compare with existing
|
44
|
+
if self.has_changes(temp_output, self.final_output):
|
45
|
+
self.prompt_user_for_confirmation()
|
46
|
+
|
47
|
+
# Move temp to final location
|
48
|
+
self.move_temp_to_final(temp_output, self.final_output)
|
49
|
+
```
|
50
|
+
|
51
|
+
## Critical Components
|
52
|
+
|
53
|
+
### client_generator.py
|
54
|
+
**Purpose**: Main entry point for all generation operations
|
55
|
+
```python
|
56
|
+
class ClientGenerator:
|
57
|
+
def __init__(self):
|
58
|
+
self.loader = SpecLoader()
|
59
|
+
self.parser = SpecParser()
|
60
|
+
self.type_service = None # Created per generation
|
61
|
+
self.visitors = self.create_visitors()
|
62
|
+
self.emitters = self.create_emitters()
|
63
|
+
self.post_processor = PostProcessManager()
|
64
|
+
|
65
|
+
def generate(self, spec_path: str, project_root: Path, output_package: str,
|
66
|
+
force: bool = False, no_postprocess: bool = False,
|
67
|
+
core_package: str = None) -> None:
|
68
|
+
"""Main generation method called by CLI"""
|
69
|
+
try:
|
70
|
+
self.validate_inputs(spec_path, project_root, output_package)
|
71
|
+
self.run_generation_pipeline(...)
|
72
|
+
except Exception as e:
|
73
|
+
raise GenerationError(f"Generation failed: {e}")
|
74
|
+
```
|
75
|
+
|
76
|
+
### Generation Workflow
|
77
|
+
```python
|
78
|
+
def run_generation_pipeline(self, spec_path: str, project_root: Path,
|
79
|
+
output_package: str, core_package: str) -> None:
|
80
|
+
|
81
|
+
# 1. Load and validate OpenAPI spec
|
82
|
+
raw_spec = self.loader.load(spec_path)
|
83
|
+
|
84
|
+
# 2. Parse to intermediate representation
|
85
|
+
ir_spec = self.parser.parse(raw_spec)
|
86
|
+
|
87
|
+
# 3. Create context for generation
|
88
|
+
context = RenderContext(project_root, output_package)
|
89
|
+
|
90
|
+
# 4. Initialize type service
|
91
|
+
self.type_service = UnifiedTypeService(ir_spec.schemas, ir_spec.responses)
|
92
|
+
|
93
|
+
# 5. Generate code using visitors
|
94
|
+
self.generate_models(ir_spec.schemas, context)
|
95
|
+
self.generate_endpoints(ir_spec.operations, context)
|
96
|
+
self.generate_client(ir_spec, context)
|
97
|
+
self.generate_exceptions(ir_spec.responses, context)
|
98
|
+
|
99
|
+
# 6. Emit files using emitters
|
100
|
+
self.emit_all_files(ir_spec, context, core_package)
|
101
|
+
|
102
|
+
# 7. Post-process (format, typecheck)
|
103
|
+
self.post_process_if_enabled(project_root, output_package)
|
104
|
+
```
|
105
|
+
|
106
|
+
## Error Handling Strategy
|
107
|
+
|
108
|
+
### 1. Structured Error Hierarchy
|
109
|
+
```python
|
110
|
+
class GenerationError(Exception):
|
111
|
+
"""Top-level error for CLI reporting"""
|
112
|
+
pass
|
113
|
+
|
114
|
+
class ValidationError(GenerationError):
|
115
|
+
"""Input validation failures"""
|
116
|
+
pass
|
117
|
+
|
118
|
+
class ParsingError(GenerationError):
|
119
|
+
"""OpenAPI parsing failures"""
|
120
|
+
pass
|
121
|
+
|
122
|
+
class CodeGenerationError(GenerationError):
|
123
|
+
"""Code generation failures"""
|
124
|
+
pass
|
125
|
+
```
|
126
|
+
|
127
|
+
### 2. Error Context Collection
|
128
|
+
```python
|
129
|
+
def handle_generation_error(self, error: Exception, context: Dict[str, Any]) -> None:
|
130
|
+
"""Add context to errors for better debugging"""
|
131
|
+
error_context = {
|
132
|
+
"spec_path": context.get("spec_path"),
|
133
|
+
"output_package": context.get("output_package"),
|
134
|
+
"current_stage": context.get("current_stage"),
|
135
|
+
"current_schema": context.get("current_schema")
|
136
|
+
}
|
137
|
+
|
138
|
+
detailed_message = f"Generation failed at {error_context['current_stage']}: {error}"
|
139
|
+
if error_context.get("current_schema"):
|
140
|
+
detailed_message += f" (processing schema: {error_context['current_schema']})"
|
141
|
+
|
142
|
+
raise GenerationError(detailed_message) from error
|
143
|
+
```
|
144
|
+
|
145
|
+
## Visitor Coordination
|
146
|
+
|
147
|
+
### 1. Visitor Initialization
|
148
|
+
```python
|
149
|
+
def create_visitors(self) -> Dict[str, Visitor]:
|
150
|
+
"""Create all visitors with proper dependencies"""
|
151
|
+
return {
|
152
|
+
"model": ModelVisitor(self.type_service),
|
153
|
+
"endpoint": EndpointVisitor(self.type_service),
|
154
|
+
"client": ClientVisitor(self.type_service),
|
155
|
+
"exception": ExceptionVisitor(self.type_service),
|
156
|
+
"docs": DocsVisitor()
|
157
|
+
}
|
158
|
+
```
|
159
|
+
|
160
|
+
### 2. Visitor Execution
|
161
|
+
```python
|
162
|
+
def generate_models(self, schemas: Dict[str, IRSchema], context: RenderContext) -> None:
|
163
|
+
"""Generate model code using visitor"""
|
164
|
+
model_codes = {}
|
165
|
+
|
166
|
+
for schema_name, schema in schemas.items():
|
167
|
+
try:
|
168
|
+
# Generate code for single schema
|
169
|
+
code = self.visitors["model"].visit_schema(schema, context)
|
170
|
+
model_codes[schema_name] = code
|
171
|
+
|
172
|
+
except Exception as e:
|
173
|
+
# Add schema context to error
|
174
|
+
context_info = {"current_schema": schema_name, "current_stage": "model_generation"}
|
175
|
+
self.handle_generation_error(e, context_info)
|
176
|
+
|
177
|
+
# Store for emitters
|
178
|
+
self.generated_models = model_codes
|
179
|
+
```
|
180
|
+
|
181
|
+
## Emitter Coordination
|
182
|
+
|
183
|
+
### 1. Emitter Initialization
|
184
|
+
```python
|
185
|
+
def create_emitters(self, output_path: Path) -> Dict[str, Emitter]:
|
186
|
+
"""Create all emitters with proper configuration"""
|
187
|
+
file_manager = FileManager(output_path)
|
188
|
+
|
189
|
+
return {
|
190
|
+
"models": ModelsEmitter(output_path, file_manager),
|
191
|
+
"endpoints": EndpointsEmitter(output_path, file_manager),
|
192
|
+
"client": ClientEmitter(output_path, file_manager),
|
193
|
+
"core": CoreEmitter(output_path, file_manager),
|
194
|
+
"exceptions": ExceptionsEmitter(output_path, file_manager)
|
195
|
+
}
|
196
|
+
```
|
197
|
+
|
198
|
+
### 2. Emitter Execution
|
199
|
+
```python
|
200
|
+
def emit_all_files(self, ir_spec: IRSpec, context: RenderContext, core_package: str) -> None:
|
201
|
+
"""Emit all generated code to files"""
|
202
|
+
|
203
|
+
# Emit in dependency order
|
204
|
+
self.emitters["core"].emit_core(context.output_package, core_package)
|
205
|
+
self.emitters["models"].emit_models(ir_spec.schemas, context)
|
206
|
+
self.emitters["endpoints"].emit_endpoints(ir_spec.operations, context)
|
207
|
+
self.emitters["exceptions"].emit_exceptions(ir_spec.responses, context)
|
208
|
+
self.emitters["client"].emit_client(ir_spec, context)
|
209
|
+
```
|
210
|
+
|
211
|
+
## Post-Processing
|
212
|
+
|
213
|
+
### 1. Format and Typecheck
|
214
|
+
```python
|
215
|
+
def post_process(self, project_root: Path, output_package: str) -> None:
|
216
|
+
"""Run Black formatting and mypy type checking"""
|
217
|
+
|
218
|
+
package_path = self.resolve_package_path(project_root, output_package)
|
219
|
+
|
220
|
+
# Format with Black
|
221
|
+
self.run_black_formatting(package_path)
|
222
|
+
|
223
|
+
# Type check with mypy
|
224
|
+
self.run_mypy_checking(package_path)
|
225
|
+
```
|
226
|
+
|
227
|
+
### 2. Validation
|
228
|
+
```python
|
229
|
+
def validate_generated_code(self, package_path: Path) -> None:
|
230
|
+
"""Validate generated code can be imported"""
|
231
|
+
|
232
|
+
# Try to import generated client
|
233
|
+
try:
|
234
|
+
spec = importlib.util.spec_from_file_location("client", package_path / "client.py")
|
235
|
+
module = importlib.util.module_from_spec(spec)
|
236
|
+
spec.loader.exec_module(module)
|
237
|
+
except Exception as e:
|
238
|
+
raise GenerationError(f"Generated code validation failed: {e}")
|
239
|
+
```
|
240
|
+
|
241
|
+
## Dependencies on Other Systems
|
242
|
+
|
243
|
+
### From core/
|
244
|
+
- Uses `SpecLoader` for OpenAPI loading
|
245
|
+
- Uses `SpecParser` for IR creation
|
246
|
+
- Uses `PostProcessManager` for formatting
|
247
|
+
|
248
|
+
### From types/
|
249
|
+
- Creates `UnifiedTypeService` for visitors
|
250
|
+
- Coordinates type resolution across generation
|
251
|
+
|
252
|
+
### From visit/
|
253
|
+
- Orchestrates all visitors
|
254
|
+
- Manages visitor dependencies
|
255
|
+
|
256
|
+
### From emitters/
|
257
|
+
- Coordinates all emitters
|
258
|
+
- Manages file output
|
259
|
+
|
260
|
+
## Testing Requirements
|
261
|
+
|
262
|
+
### Integration Tests
|
263
|
+
```python
|
264
|
+
def test_client_generator__complete_generation__creates_working_client():
|
265
|
+
# Test full generation pipeline
|
266
|
+
generator = ClientGenerator()
|
267
|
+
|
268
|
+
# Generate client
|
269
|
+
generator.generate(
|
270
|
+
spec_path="test_spec.yaml",
|
271
|
+
project_root=temp_dir,
|
272
|
+
output_package="test_client"
|
273
|
+
)
|
274
|
+
|
275
|
+
# Verify client works
|
276
|
+
assert can_import_client(temp_dir / "test_client")
|
277
|
+
assert client_methods_work(temp_dir / "test_client")
|
278
|
+
```
|
279
|
+
|
280
|
+
### Error Handling Tests
|
281
|
+
```python
|
282
|
+
def test_client_generator__invalid_spec__raises_generation_error():
|
283
|
+
generator = ClientGenerator()
|
284
|
+
|
285
|
+
with pytest.raises(GenerationError) as exc_info:
|
286
|
+
generator.generate(
|
287
|
+
spec_path="invalid_spec.yaml",
|
288
|
+
project_root=temp_dir,
|
289
|
+
output_package="test_client"
|
290
|
+
)
|
291
|
+
|
292
|
+
assert "parsing failed" in str(exc_info.value)
|
293
|
+
```
|
294
|
+
|
295
|
+
## Extension Points
|
296
|
+
|
297
|
+
### Custom Generation Steps
|
298
|
+
```python
|
299
|
+
class CustomClientGenerator(ClientGenerator):
|
300
|
+
def run_generation_pipeline(self, *args, **kwargs):
|
301
|
+
# Add custom pre-processing
|
302
|
+
self.custom_pre_process()
|
303
|
+
|
304
|
+
# Run standard pipeline
|
305
|
+
super().run_generation_pipeline(*args, **kwargs)
|
306
|
+
|
307
|
+
# Add custom post-processing
|
308
|
+
self.custom_post_process()
|
309
|
+
```
|
310
|
+
|
311
|
+
### Plugin System
|
312
|
+
```python
|
313
|
+
def register_plugin(self, plugin: GenerationPlugin) -> None:
|
314
|
+
"""Register custom generation plugin"""
|
315
|
+
self.plugins.append(plugin)
|
316
|
+
|
317
|
+
def run_plugins(self, stage: str, context: Dict[str, Any]) -> None:
|
318
|
+
"""Run plugins at specific generation stage"""
|
319
|
+
for plugin in self.plugins:
|
320
|
+
if plugin.handles_stage(stage):
|
321
|
+
plugin.execute(context)
|
322
|
+
```
|
323
|
+
|
324
|
+
## Critical Implementation Details
|
325
|
+
|
326
|
+
### Resource Management
|
327
|
+
```python
|
328
|
+
def generate_safely(self, *args, **kwargs) -> None:
|
329
|
+
"""Generate with proper resource cleanup"""
|
330
|
+
temp_files = []
|
331
|
+
|
332
|
+
try:
|
333
|
+
# Generation logic
|
334
|
+
pass
|
335
|
+
except Exception:
|
336
|
+
# Clean up temporary files
|
337
|
+
for temp_file in temp_files:
|
338
|
+
if temp_file.exists():
|
339
|
+
temp_file.unlink()
|
340
|
+
raise
|
341
|
+
```
|
342
|
+
|
343
|
+
### Progress Reporting
|
344
|
+
```python
|
345
|
+
def generate_with_progress(self, *args, **kwargs) -> None:
|
346
|
+
"""Generate with progress reporting"""
|
347
|
+
stages = ["loading", "parsing", "code_generation", "file_emission", "post_processing"]
|
348
|
+
|
349
|
+
for i, stage in enumerate(stages):
|
350
|
+
print(f"[{i+1}/{len(stages)}] {stage.replace('_', ' ').title()}...")
|
351
|
+
self.run_stage(stage)
|
352
|
+
```
|