pyopenapi-gen 2.7.2__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.
Files changed (137) hide show
  1. pyopenapi_gen/__init__.py +224 -0
  2. pyopenapi_gen/__main__.py +6 -0
  3. pyopenapi_gen/cli.py +62 -0
  4. pyopenapi_gen/context/CLAUDE.md +284 -0
  5. pyopenapi_gen/context/file_manager.py +52 -0
  6. pyopenapi_gen/context/import_collector.py +382 -0
  7. pyopenapi_gen/context/render_context.py +726 -0
  8. pyopenapi_gen/core/CLAUDE.md +224 -0
  9. pyopenapi_gen/core/__init__.py +0 -0
  10. pyopenapi_gen/core/auth/base.py +22 -0
  11. pyopenapi_gen/core/auth/plugins.py +89 -0
  12. pyopenapi_gen/core/cattrs_converter.py +810 -0
  13. pyopenapi_gen/core/exceptions.py +20 -0
  14. pyopenapi_gen/core/http_status_codes.py +218 -0
  15. pyopenapi_gen/core/http_transport.py +222 -0
  16. pyopenapi_gen/core/loader/__init__.py +12 -0
  17. pyopenapi_gen/core/loader/loader.py +174 -0
  18. pyopenapi_gen/core/loader/operations/__init__.py +12 -0
  19. pyopenapi_gen/core/loader/operations/parser.py +161 -0
  20. pyopenapi_gen/core/loader/operations/post_processor.py +62 -0
  21. pyopenapi_gen/core/loader/operations/request_body.py +90 -0
  22. pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
  23. pyopenapi_gen/core/loader/parameters/parser.py +186 -0
  24. pyopenapi_gen/core/loader/responses/__init__.py +10 -0
  25. pyopenapi_gen/core/loader/responses/parser.py +111 -0
  26. pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
  27. pyopenapi_gen/core/loader/schemas/extractor.py +275 -0
  28. pyopenapi_gen/core/pagination.py +64 -0
  29. pyopenapi_gen/core/parsing/__init__.py +13 -0
  30. pyopenapi_gen/core/parsing/common/__init__.py +1 -0
  31. pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
  32. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
  33. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
  34. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
  35. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
  36. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
  37. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
  38. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
  39. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
  40. pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
  41. pyopenapi_gen/core/parsing/common/type_parser.py +73 -0
  42. pyopenapi_gen/core/parsing/context.py +187 -0
  43. pyopenapi_gen/core/parsing/cycle_helpers.py +126 -0
  44. pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
  45. pyopenapi_gen/core/parsing/keywords/all_of_parser.py +81 -0
  46. pyopenapi_gen/core/parsing/keywords/any_of_parser.py +84 -0
  47. pyopenapi_gen/core/parsing/keywords/array_items_parser.py +72 -0
  48. pyopenapi_gen/core/parsing/keywords/one_of_parser.py +77 -0
  49. pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
  50. pyopenapi_gen/core/parsing/schema_finalizer.py +169 -0
  51. pyopenapi_gen/core/parsing/schema_parser.py +804 -0
  52. pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
  53. pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
  54. pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +120 -0
  55. pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
  56. pyopenapi_gen/core/postprocess_manager.py +260 -0
  57. pyopenapi_gen/core/spec_fetcher.py +148 -0
  58. pyopenapi_gen/core/streaming_helpers.py +84 -0
  59. pyopenapi_gen/core/telemetry.py +69 -0
  60. pyopenapi_gen/core/utils.py +456 -0
  61. pyopenapi_gen/core/warning_collector.py +83 -0
  62. pyopenapi_gen/core/writers/code_writer.py +135 -0
  63. pyopenapi_gen/core/writers/documentation_writer.py +222 -0
  64. pyopenapi_gen/core/writers/line_writer.py +217 -0
  65. pyopenapi_gen/core/writers/python_construct_renderer.py +321 -0
  66. pyopenapi_gen/core_package_template/README.md +21 -0
  67. pyopenapi_gen/emit/models_emitter.py +143 -0
  68. pyopenapi_gen/emitters/CLAUDE.md +286 -0
  69. pyopenapi_gen/emitters/client_emitter.py +51 -0
  70. pyopenapi_gen/emitters/core_emitter.py +181 -0
  71. pyopenapi_gen/emitters/docs_emitter.py +44 -0
  72. pyopenapi_gen/emitters/endpoints_emitter.py +247 -0
  73. pyopenapi_gen/emitters/exceptions_emitter.py +187 -0
  74. pyopenapi_gen/emitters/mocks_emitter.py +185 -0
  75. pyopenapi_gen/emitters/models_emitter.py +426 -0
  76. pyopenapi_gen/generator/CLAUDE.md +352 -0
  77. pyopenapi_gen/generator/client_generator.py +567 -0
  78. pyopenapi_gen/generator/exceptions.py +7 -0
  79. pyopenapi_gen/helpers/CLAUDE.md +325 -0
  80. pyopenapi_gen/helpers/__init__.py +1 -0
  81. pyopenapi_gen/helpers/endpoint_utils.py +532 -0
  82. pyopenapi_gen/helpers/type_cleaner.py +334 -0
  83. pyopenapi_gen/helpers/type_helper.py +112 -0
  84. pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
  85. pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
  86. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
  87. pyopenapi_gen/helpers/type_resolution/finalizer.py +105 -0
  88. pyopenapi_gen/helpers/type_resolution/named_resolver.py +172 -0
  89. pyopenapi_gen/helpers/type_resolution/object_resolver.py +216 -0
  90. pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +109 -0
  91. pyopenapi_gen/helpers/type_resolution/resolver.py +47 -0
  92. pyopenapi_gen/helpers/url_utils.py +14 -0
  93. pyopenapi_gen/http_types.py +20 -0
  94. pyopenapi_gen/ir.py +165 -0
  95. pyopenapi_gen/py.typed +1 -0
  96. pyopenapi_gen/types/CLAUDE.md +140 -0
  97. pyopenapi_gen/types/__init__.py +11 -0
  98. pyopenapi_gen/types/contracts/__init__.py +13 -0
  99. pyopenapi_gen/types/contracts/protocols.py +106 -0
  100. pyopenapi_gen/types/contracts/types.py +28 -0
  101. pyopenapi_gen/types/resolvers/__init__.py +7 -0
  102. pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
  103. pyopenapi_gen/types/resolvers/response_resolver.py +177 -0
  104. pyopenapi_gen/types/resolvers/schema_resolver.py +498 -0
  105. pyopenapi_gen/types/services/__init__.py +5 -0
  106. pyopenapi_gen/types/services/type_service.py +165 -0
  107. pyopenapi_gen/types/strategies/__init__.py +5 -0
  108. pyopenapi_gen/types/strategies/response_strategy.py +310 -0
  109. pyopenapi_gen/visit/CLAUDE.md +272 -0
  110. pyopenapi_gen/visit/client_visitor.py +477 -0
  111. pyopenapi_gen/visit/docs_visitor.py +38 -0
  112. pyopenapi_gen/visit/endpoint/__init__.py +1 -0
  113. pyopenapi_gen/visit/endpoint/endpoint_visitor.py +292 -0
  114. pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
  115. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +123 -0
  116. pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +222 -0
  117. pyopenapi_gen/visit/endpoint/generators/mock_generator.py +140 -0
  118. pyopenapi_gen/visit/endpoint/generators/overload_generator.py +252 -0
  119. pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
  120. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +705 -0
  121. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +83 -0
  122. pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +207 -0
  123. pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
  124. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +78 -0
  125. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
  126. pyopenapi_gen/visit/exception_visitor.py +90 -0
  127. pyopenapi_gen/visit/model/__init__.py +0 -0
  128. pyopenapi_gen/visit/model/alias_generator.py +93 -0
  129. pyopenapi_gen/visit/model/dataclass_generator.py +553 -0
  130. pyopenapi_gen/visit/model/enum_generator.py +212 -0
  131. pyopenapi_gen/visit/model/model_visitor.py +198 -0
  132. pyopenapi_gen/visit/visitor.py +97 -0
  133. pyopenapi_gen-2.7.2.dist-info/METADATA +1169 -0
  134. pyopenapi_gen-2.7.2.dist-info/RECORD +137 -0
  135. pyopenapi_gen-2.7.2.dist-info/WHEEL +4 -0
  136. pyopenapi_gen-2.7.2.dist-info/entry_points.txt +2 -0
  137. pyopenapi_gen-2.7.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,321 @@
1
+ """
2
+ PythonConstructRenderer: Renders Python language constructs like classes, enums, and type aliases.
3
+
4
+ This module provides the PythonConstructRenderer class, which is responsible for
5
+ generating well-formatted Python code for common constructs used in the generated client.
6
+ It handles all the details of formatting, import registration, and docstring generation
7
+ for these constructs.
8
+ """
9
+
10
+ from typing import List, Tuple
11
+
12
+ from pyopenapi_gen.context.render_context import RenderContext
13
+
14
+ from .code_writer import CodeWriter
15
+ from .documentation_writer import DocumentationBlock, DocumentationWriter
16
+
17
+
18
+ class PythonConstructRenderer:
19
+ """
20
+ Generates Python code for common constructs like dataclasses, enums, and type aliases.
21
+
22
+ This class provides methods to render different Python language constructs with
23
+ proper formatting, documentation, and import handling. It uses CodeWriter and
24
+ DocumentationWriter internally to ensure consistent output and automatically
25
+ registers necessary imports into the provided RenderContext.
26
+
27
+ The renderer handles:
28
+ - Type aliases (e.g., UserId = str)
29
+ - Enums (with str or int values)
30
+ - Dataclasses (with required and optional fields)
31
+ - Generic classes (with bases, docstrings, and body)
32
+ """
33
+
34
+ def render_alias(
35
+ self,
36
+ alias_name: str,
37
+ target_type: str,
38
+ description: str | None,
39
+ context: RenderContext,
40
+ ) -> str:
41
+ """
42
+ Render a type alias assignment as Python code.
43
+
44
+ Args:
45
+ alias_name: The name for the type alias
46
+ target_type: The target type expression
47
+ description: Optional description for the docstring
48
+ context: The rendering context for import registration
49
+
50
+ Returns:
51
+ Formatted Python code for the type alias
52
+
53
+ Example:
54
+ ```python
55
+ # Assuming: from typing import TypeAlias
56
+ UserId: TypeAlias = str
57
+ '''Alias for a user identifier''' # Reverted to triple-single for example
58
+ ```
59
+ """
60
+ writer = CodeWriter()
61
+ # Add TypeAlias import
62
+ context.add_import("typing", "TypeAlias")
63
+ # Register imports needed by the target type itself
64
+ context.add_typing_imports_for_type(target_type)
65
+
66
+ # Add __all__ export
67
+ writer.write_line(f'__all__ = ["{alias_name}"]')
68
+ writer.write_line("") # Add a blank line for separation
69
+
70
+ writer.write_line(f"{alias_name}: TypeAlias = {target_type}")
71
+ if description:
72
+ # Sanitize description for use within a triple-double-quoted string for the actual docstring
73
+ safe_desc_content = description.replace("\\", "\\\\") # Escape backslashes first
74
+ safe_desc_content = safe_desc_content.replace('"""', '\\"\\"\\"') # Escape triple-double-quotes
75
+ writer.write_line(f'"""Alias for {safe_desc_content}"""') # Actual generated docstring uses """
76
+ return writer.get_code()
77
+
78
+ def render_enum(
79
+ self,
80
+ enum_name: str,
81
+ base_type: str, # 'str' or 'int'
82
+ values: List[Tuple[str, str | int]], # List of (MEMBER_NAME, value)
83
+ description: str | None,
84
+ context: RenderContext,
85
+ ) -> str:
86
+ """
87
+ Render an Enum class as Python code.
88
+
89
+ Args:
90
+ enum_name: The name of the enum class
91
+ base_type: The base type, either 'str' or 'int'
92
+ values: List of (member_name, value) pairs
93
+ description: Optional description for the docstring
94
+ context: The rendering context for import registration
95
+
96
+ Returns:
97
+ Formatted Python code for the enum class
98
+
99
+ Example:
100
+ ```python
101
+ @unique
102
+ class Color(str, Enum):
103
+ \"\"\"Color options available in the API\"\"\"
104
+ RED = "red"
105
+ GREEN = "green"
106
+ BLUE = "blue"
107
+ ```
108
+ """
109
+ writer = CodeWriter()
110
+ context.add_import("enum", "Enum")
111
+ context.add_import("enum", "unique")
112
+
113
+ # Add __all__ export
114
+ writer.write_line(f'__all__ = ["{enum_name}"]')
115
+ writer.write_line("") # Add a blank line for separation
116
+
117
+ writer.write_line("@unique")
118
+ writer.write_line(f"class {enum_name}(" + base_type + ", Enum):")
119
+ writer.indent()
120
+
121
+ # Build and write docstring
122
+ doc_args: list[tuple[str, str, str] | tuple[str, str]] = []
123
+ for member_name, value in values:
124
+ doc_args.append((str(value), base_type, f"Value for {member_name}"))
125
+ doc_block = DocumentationBlock(
126
+ summary=description or f"{enum_name} Enum",
127
+ args=doc_args if doc_args else None,
128
+ )
129
+ docstring = DocumentationWriter(width=88).render_docstring(doc_block, indent=0)
130
+ for line in docstring.splitlines():
131
+ writer.write_line(line)
132
+
133
+ # Write Enum members
134
+ for member_name, value in values:
135
+ if base_type == "str":
136
+ writer.write_line(f'{member_name} = "{value}"')
137
+ else: # int
138
+ writer.write_line(f"{member_name} = {value}")
139
+
140
+ writer.dedent()
141
+ return writer.get_code()
142
+
143
+ def render_dataclass(
144
+ self,
145
+ class_name: str,
146
+ fields: List[Tuple[str, str, str | None, str | None]], # name, type_hint, default_expr, description
147
+ description: str | None,
148
+ context: RenderContext,
149
+ field_mappings: dict[str, str] | None = None,
150
+ ) -> str:
151
+ """
152
+ Render a dataclass as Python code with cattrs field mapping support.
153
+
154
+ Args:
155
+ class_name: The name of the dataclass
156
+ fields: List of (name, type_hint, default_expr, description) tuples for each field
157
+ description: Optional description for the class docstring
158
+ context: The rendering context for import registration
159
+ field_mappings: Optional mapping of API field names to Python field names (Meta class)
160
+
161
+ Returns:
162
+ Formatted Python code for the dataclass
163
+
164
+ Example:
165
+ ```python
166
+ @dataclass
167
+ class User:
168
+ \"\"\"User information with automatic JSON field mapping via cattrs.\"\"\"
169
+ id_: str
170
+ first_name: str
171
+ email: str | None = None
172
+ is_active: bool = True
173
+
174
+ class Meta:
175
+ \"\"\"Configure field name mapping for JSON conversion.\"\"\"
176
+ key_transform_with_load = {
177
+ 'id': 'id_',
178
+ 'firstName': 'first_name'
179
+ }
180
+ ```
181
+ """
182
+ writer = CodeWriter()
183
+ context.add_import("dataclasses", "dataclass")
184
+
185
+ # No BaseSchema needed - using cattrs for serialization
186
+ # Field mappings will be handled by cattrs converter
187
+
188
+ # Add __all__ export
189
+ writer.write_line(f'__all__ = ["{class_name}"]')
190
+ writer.write_line("") # Add a blank line for separation
191
+
192
+ writer.write_line("@dataclass")
193
+ writer.write_line(f"class {class_name}:")
194
+ writer.indent()
195
+
196
+ # Build and write docstring
197
+ field_args: list[tuple[str, str, str] | tuple[str, str]] = []
198
+ for name, type_hint, _, field_desc in fields:
199
+ field_args.append((name, type_hint, field_desc or ""))
200
+
201
+ # Simple description
202
+ base_description = description or f"{class_name} dataclass"
203
+ enhanced_description = base_description
204
+
205
+ doc_block = DocumentationBlock(
206
+ summary=enhanced_description,
207
+ args=field_args if field_args else None,
208
+ )
209
+ docstring = DocumentationWriter(width=88).render_docstring(doc_block, indent=0)
210
+ for line in docstring.splitlines():
211
+ writer.write_line(line)
212
+
213
+ # Write fields
214
+ if not fields:
215
+ writer.write_line("# No properties defined in schema")
216
+ writer.write_line("pass")
217
+ else:
218
+ # Separate required and optional fields for correct ordering (no defaults first)
219
+ required_fields = [f for f in fields if f[2] is None] # default_expr is None
220
+ optional_fields = [f for f in fields if f[2] is not None] # default_expr is not None
221
+
222
+ # Required fields
223
+ for name, type_hint, _, field_desc in required_fields:
224
+ line = f"{name}: {type_hint}"
225
+ if field_desc:
226
+ comment_text = field_desc.replace("\n", " ")
227
+ line += f" # {comment_text}"
228
+ writer.write_line(line)
229
+
230
+ # Optional fields
231
+ for name, type_hint, default_expr, field_desc in optional_fields:
232
+ if default_expr and "default_factory" in default_expr:
233
+ context.add_import("dataclasses", "field") # Ensure field is imported
234
+ line = f"{name}: {type_hint} = {default_expr}"
235
+ if field_desc:
236
+ comment_text = field_desc.replace("\n", " ")
237
+ line += f" # {comment_text}"
238
+ writer.write_line(line)
239
+
240
+ # Add Meta class if field mappings are provided (for cattrs field mapping)
241
+ if field_mappings:
242
+ writer.write_line("") # Blank line before Meta class
243
+ writer.write_line("class Meta:")
244
+ writer.indent()
245
+ writer.write_line('"""Configure field name mapping for JSON conversion."""')
246
+
247
+ # key_transform_with_load: API field name -> Python field name (for deserialization)
248
+ writer.write_line("key_transform_with_load = {")
249
+ writer.indent()
250
+ for api_field, python_field in sorted(field_mappings.items()):
251
+ writer.write_line(f'"{api_field}": "{python_field}",')
252
+ writer.dedent()
253
+ writer.write_line("}")
254
+
255
+ # key_transform_with_dump: Python field name -> API field name (for serialization)
256
+ writer.write_line("key_transform_with_dump = {")
257
+ writer.indent()
258
+ # Reverse the mapping for dump
259
+ for api_field, python_field in sorted(field_mappings.items(), key=lambda x: x[1]):
260
+ writer.write_line(f'"{python_field}": "{api_field}",')
261
+ writer.dedent()
262
+ writer.write_line("}")
263
+
264
+ writer.dedent()
265
+
266
+ writer.dedent()
267
+ return writer.get_code()
268
+
269
+ def render_class(
270
+ self,
271
+ class_name: str,
272
+ base_classes: List[str] | None,
273
+ docstring: str | None,
274
+ body_lines: List[str] | None,
275
+ context: RenderContext,
276
+ ) -> str:
277
+ """
278
+ Render a generic class definition as Python code.
279
+
280
+ Args:
281
+ class_name: The name of the class
282
+ base_classes: Optional list of base class names (for inheritance)
283
+ docstring: Optional class docstring content
284
+ body_lines: Optional list of code lines for the class body
285
+ context: The rendering context (not used for generic classes)
286
+
287
+ Returns:
288
+ Formatted Python code for the class
289
+
290
+ Example:
291
+ ```python
292
+ class CustomError(Exception):
293
+ \"\"\"Raised when a custom error occurs.\"\"\"
294
+ def __init__(self, message: str, code: int):
295
+ self.code = code
296
+ super().__init__(message)
297
+ ```
298
+ """
299
+ writer = CodeWriter()
300
+ bases = f"({', '.join(base_classes)})" if base_classes else ""
301
+ writer.write_line(f"class {class_name}{bases}:")
302
+ writer.indent()
303
+ has_content = False
304
+ if docstring:
305
+ # Simple triple-quoted docstring is sufficient for exceptions
306
+ writer.write_line(f'"""{docstring}"""')
307
+ has_content = True
308
+ if body_lines:
309
+ for line in body_lines:
310
+ # Handle empty lines without adding indentation (Ruff W293)
311
+ if line == "":
312
+ writer.writer.newline() # Just add a newline, no indent
313
+ else:
314
+ writer.write_line(line)
315
+ has_content = True
316
+
317
+ if not has_content:
318
+ writer.write_line("pass") # Need pass if class is completely empty
319
+
320
+ writer.dedent()
321
+ return writer.get_code()
@@ -0,0 +1,21 @@
1
+ # Core Runtime Components
2
+
3
+ This directory contains the core runtime components required by the generated API client.
4
+
5
+ ## Client Independence
6
+
7
+ **The generated API client is fully independent and does NOT require the `pyopenapi_gen` package to be installed at runtime.**
8
+
9
+ All necessary base classes, protocols, transport implementations, and utility functions are included directly within this `core` package. You can safely use the generated client in any Python environment without installing the generator itself.
10
+
11
+ ## Shared Core (Optional)
12
+
13
+ In scenarios where multiple API clients are generated (perhaps for different services within the same ecosystem), you might want to share a single instance of this `core` package to avoid duplication.
14
+
15
+ To achieve this:
16
+ 1. Generate the first client normally. This will create the `core` package.
17
+ 2. Move or copy this `core` package to a location accessible by all your client packages (e.g., a shared `libs` directory).
18
+ 3. Ensure this shared location is included in your Python environment's `PYTHONPATH`.
19
+ 4. When generating subsequent clients, use the `--core-import-path <path_to_shared_core>` option with `pyopenapi-gen`. This tells the generator *not* to create a new `core` directory but instead to generate imports relative to the specified shared path (e.g., `from shared_libs.core.http_transport import HttpTransport`).
20
+
21
+ This allows multiple clients to reuse the same base implementation, reducing code size and ensuring consistency.
@@ -0,0 +1,143 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import List
4
+
5
+ from pyopenapi_gen.context.render_context import RenderContext
6
+ from pyopenapi_gen.core.utils import NameSanitizer
7
+ from pyopenapi_gen.core.writers.code_writer import CodeWriter
8
+ from pyopenapi_gen.ir import IRSchema
9
+ from pyopenapi_gen.visit.model.model_visitor import ModelVisitor
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class ModelsEmitter:
15
+ """Emits model files (dataclasses, enums) from IRSchema definitions."""
16
+
17
+ def __init__(self, context: RenderContext):
18
+ self.context = context
19
+ # self.import_collector is available via self.context.import_collector
20
+ # self.writer an instance CodeWriter() here seems unused globally for this emitter.
21
+ # Each file generation part either writes directly or uses a local CodeWriter.
22
+
23
+ def _generate_model_file(self, schema_ir: IRSchema, models_dir: Path) -> str | None:
24
+ """Generates a single Python file for a given IRSchema. Returns file path if generated."""
25
+ if not schema_ir.name:
26
+ logger.warning(f"Skipping model generation for schema without a name: {schema_ir}")
27
+ return None
28
+
29
+ # Ensure context has parsed_schemas before ModelVisitor uses it
30
+ if not self.context.parsed_schemas:
31
+ logger.error(
32
+ "[ModelsEmitter._generate_model_file] RenderContext is missing parsed_schemas. Cannot generate model."
33
+ )
34
+ return None
35
+
36
+ module_name = NameSanitizer.sanitize_module_name(schema_ir.name)
37
+ # class_name = NameSanitizer.sanitize_class_name(schema_ir.name) # Not directly used here for file content
38
+ file_path = models_dir / f"{module_name}.py"
39
+
40
+ # Set current file on RenderContext. This also resets its internal ImportCollector.
41
+ self.context.set_current_file(str(file_path))
42
+
43
+ # ModelsEmitter's import_collector should be the same instance as RenderContext's.
44
+ # The line `self.import_collector = self.context.import_collector` was added in __init__
45
+ # or after set_current_file previously. Let's ensure it's correctly synced if there was any doubt.
46
+ current_ic = self.context.import_collector # Use the collector from the context
47
+
48
+ # Instantiate ModelVisitor
49
+ # ModelVisitor will use self.context (and thus current_ic) to add imports.
50
+ visitor = ModelVisitor(schemas=self.context.parsed_schemas)
51
+ rendered_model_str = visitor.visit(schema_ir, self.context)
52
+
53
+ # Get collected imports for the current file.
54
+ imports_list = current_ic.get_import_statements()
55
+ imports_code = "\n".join(imports_list)
56
+
57
+ # The model_code is what the visitor returned.
58
+ model_code = rendered_model_str
59
+
60
+ full_code = imports_code + "\n\n" + model_code if imports_code else model_code
61
+
62
+ file_path.parent.mkdir(parents=True, exist_ok=True)
63
+ with file_path.open("w", encoding="utf-8") as f:
64
+ f.write(full_code)
65
+ # self.writer.clear() # ModelsEmitter's self.writer is not used for individual model file body
66
+ # logger.debug(f"Generated model file: {file_path} for schema: {schema_ir.name}")
67
+ return str(file_path)
68
+
69
+ def _generate_init_py(self, models_dir: Path) -> str:
70
+ """Generates the models/__init__.py file and returns its path."""
71
+ init_writer = CodeWriter()
72
+ # ... (content generation as before) ...
73
+ all_class_names: List[str] = []
74
+ # Ensure self.context.parsed_schemas is not None before iterating
75
+ if not self.context.parsed_schemas:
76
+ logger.warning("No parsed schemas found in context for generating models/__init__.py")
77
+ # Still create an empty __init__.py with basic imports
78
+ init_writer.write_line("from typing import List, Optional, Union, Any, Dict, Generic, TypeVar")
79
+ init_writer.write_line("from dataclasses import dataclass, field")
80
+ init_writer.write_line("__all__ = []")
81
+ init_content = str(init_writer)
82
+ init_file_path = models_dir / "__init__.py"
83
+ with init_file_path.open("w", encoding="utf-8") as f:
84
+ f.write(init_content)
85
+ return str(init_file_path)
86
+
87
+ # This point onwards, self.context.parsed_schemas is guaranteed to be non-None
88
+ sorted_schema_items = sorted(self.context.parsed_schemas.items())
89
+
90
+ for schema_key, s in sorted_schema_items:
91
+ if not s.name:
92
+ logger.warning(f"Schema with key '{schema_key}' has no name, skipping for __init__.py")
93
+ continue
94
+ module_name = NameSanitizer.sanitize_module_name(s.name)
95
+ class_name = NameSanitizer.sanitize_class_name(s.name)
96
+ if module_name == "__init__":
97
+ logger.warning(f"Skipping import for schema '{s.name}' as its module name is __init__.")
98
+ continue
99
+ init_writer.write_line(f"from .{module_name} import {class_name}")
100
+ all_class_names.append(class_name)
101
+
102
+ init_writer.write_line("from typing import List, Optional, Union, Any, Dict, Generic, TypeVar")
103
+ init_writer.write_line("from dataclasses import dataclass, field")
104
+ init_writer.write_line("")
105
+
106
+ # Re-add imports for each class to ensure they are structured correctly by CodeWriter
107
+ # This is a bit redundant if CodeWriter handles sections, but safe.
108
+ # For __init__.py, it might be simpler to just list exports.
109
+
110
+ init_writer.write_line("")
111
+ init_writer.write_line("__all__ = [")
112
+ for name in sorted(all_class_names):
113
+ init_writer.write_line(f" '{name}',")
114
+ init_writer.write_line("]")
115
+ init_content = str(init_writer) # This needs to be CodeWriter.render() or similar if sections are used
116
+
117
+ init_file_path = models_dir / "__init__.py"
118
+ with init_file_path.open("w", encoding="utf-8") as f:
119
+ f.write(init_content)
120
+ return str(init_file_path)
121
+
122
+ def emit(self, output_base_dir: Path) -> List[str]:
123
+ """Emits all model files and the models/__init__.py file into <output_base_dir>/models/."""
124
+ models_dir = output_base_dir / "models"
125
+ models_dir.mkdir(parents=True, exist_ok=True)
126
+ generated_files: List[str] = []
127
+
128
+ if not self.context.parsed_schemas:
129
+ logger.warning("No parsed schemas found in context. Skipping model file generation.")
130
+ else:
131
+ sorted_schemas = sorted(self.context.parsed_schemas.values(), key=lambda s: s.name or "")
132
+ for schema_ir in sorted_schemas:
133
+ if schema_ir.name:
134
+ file_path = self._generate_model_file(schema_ir, models_dir)
135
+ if file_path:
136
+ generated_files.append(file_path)
137
+ else:
138
+ # logger.debug(f"Skipping file generation for unnamed schema: {schema_ir.type}")
139
+ pass # Schema has no name, already warned in _generate_model_file or skipped if truly unnamed
140
+
141
+ init_py_path = self._generate_init_py(models_dir)
142
+ generated_files.append(init_py_path)
143
+ return generated_files