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,382 @@
1
+ """
2
+ ImportCollector: Manages imports for generated Python modules.
3
+
4
+ This module provides the ImportCollector class, which collects, organizes, and formats
5
+ import statements for Python modules. It supports various import styles, including standard,
6
+ direct, relative, and plain imports, with methods to add and query import statements.
7
+ """
8
+
9
+ import logging
10
+ import sys
11
+ from collections import defaultdict
12
+ from typing import List, Set
13
+
14
+ # Initialize module logger
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Standard library modules for _is_stdlib check
18
+ COMMON_STDLIB = {
19
+ "typing",
20
+ "os",
21
+ "sys",
22
+ "re",
23
+ "json",
24
+ "collections",
25
+ "datetime",
26
+ "enum",
27
+ "pathlib",
28
+ "abc",
29
+ "contextlib",
30
+ "functools",
31
+ "itertools",
32
+ "logging",
33
+ "math",
34
+ "decimal",
35
+ "dataclasses",
36
+ "asyncio",
37
+ "tempfile",
38
+ "subprocess",
39
+ "textwrap",
40
+ }
41
+
42
+ # Stdlib modules that should prefer \'import module\' over \'from module import module\'
43
+ # when add_import(module, module) is called.
44
+ STDLIB_MODULES_PREFER_PLAIN_IMPORT_WHEN_NAME_MATCHES = {
45
+ "os",
46
+ "sys",
47
+ "re",
48
+ "json",
49
+ "contextlib",
50
+ "functools",
51
+ "itertools",
52
+ "logging",
53
+ "math",
54
+ "asyncio",
55
+ "tempfile",
56
+ "subprocess",
57
+ "textwrap",
58
+ }
59
+
60
+
61
+ def _is_stdlib(module_name: str) -> bool:
62
+ """Check if a module is part of the standard library."""
63
+ top_level_module = module_name.split(".")[0]
64
+ return module_name in sys.builtin_module_names or module_name in COMMON_STDLIB or top_level_module in COMMON_STDLIB
65
+
66
+
67
+ def make_relative_import(current_module_dot_path: str, target_module_dot_path: str) -> str:
68
+ """Generate a relative import path string from current_module to target_module."""
69
+ current_parts = current_module_dot_path.split(".")
70
+ target_parts = target_module_dot_path.split(".")
71
+
72
+ current_dir_parts = current_parts[:-1]
73
+
74
+ # Calculate common prefix length (L) between current_dir_parts and the full target_parts
75
+ L = 0
76
+ while L < len(current_dir_parts) and L < len(target_parts) and current_dir_parts[L] == target_parts[L]:
77
+ L += 1
78
+
79
+ # Number of levels to go "up" from current_module's directory to the common ancestor with target.
80
+ up_levels = len(current_dir_parts) - L
81
+
82
+ # The remaining components of the target path, after this common prefix L.
83
+ remaining_target_components = target_parts[L:]
84
+
85
+ if up_levels == 0:
86
+ # This means the common prefix L makes current_dir_parts a prefix of (or same as)
87
+ # target_parts's directory structure portion.
88
+ # Or, target is in a subdirectory of current_dir_parts[L-1]
89
+
90
+ # Special case for importing a submodule from its parent package's __init__.py
91
+ # e.g. current="pkg.sub" (representing pkg/sub/__init__.py), target="pkg.sub.mod"
92
+ # Expected: ".mod"
93
+ is_direct_package_import = len(current_parts) < len(target_parts) and target_module_dot_path.startswith(
94
+ current_module_dot_path + "."
95
+ )
96
+
97
+ if is_direct_package_import:
98
+ # current_parts = [pkg, sub], target_parts = [pkg, sub, mod]
99
+ # We want target_parts after current_parts, i.e., [mod]
100
+ final_suffix_parts = target_parts[len(current_parts) :]
101
+ else:
102
+ # General case for up_levels == 0.
103
+ # e.g. current="pkg.mod1" (dir pkg), target="pkg.mod2" (dir pkg)
104
+ # current_dir_parts=[pkg], target_parts=[pkg,mod2]. L=1 (for pkg).
105
+ # up_levels = 1-1=0. remaining_target_components=target_parts[1:]=[mod2]. -> .mod2
106
+ # e.g. current="pkg.mod1" (dir pkg), target="pkg.sub.mod2" (dir pkg.sub)
107
+ # current_dir_parts=[pkg], target_parts=[pkg,sub,mod2]. L=1.
108
+ # up_levels = 0. remaining_target_components=target_parts[1:]=[sub,mod2]. -> .sub.mod2
109
+ final_suffix_parts = remaining_target_components
110
+
111
+ return "." + ".".join(final_suffix_parts)
112
+ else: # up_levels >= 1
113
+ # up_levels = 1 means one step up ("..")
114
+ # up_levels = N means N steps up (N+1 dots)
115
+ return ("." * (up_levels + 1)) + ".".join(remaining_target_components)
116
+
117
+
118
+ class ImportCollector:
119
+ """
120
+ Manages imports for generated Python modules.
121
+
122
+ This class collects and organizes imports in a structured way, ensuring
123
+ consistency across all generated files. It provides methods to add different
124
+ types of imports and generate properly formatted import statements.
125
+
126
+ Attributes:
127
+ imports: Dictionary mapping module names to sets of imported names
128
+ (for standard imports like `from typing import List`)
129
+ direct_imports: Dictionary for direct imports (similar to imports)
130
+ relative_imports: Dictionary for relative imports (like `from .models import Pet`)
131
+ plain_imports: Set of module names for plain imports (like `import json`)
132
+
133
+ Example usage:
134
+ imports = ImportCollector()
135
+ imports.add_import("dataclasses", "dataclass")
136
+ imports.add_typing_import("Optional")
137
+ imports.add_typing_import("List")
138
+
139
+ for statement in imports.get_import_statements():
140
+ print(statement)
141
+ """
142
+
143
+ def __init__(self) -> None:
144
+ """Initialize a new ImportCollector with empty collections for all import types."""
145
+ # Standard imports (from x import y)
146
+ self.imports: dict[str, Set[str]] = {}
147
+ # Direct imports like 'from datetime import date'
148
+ # self.direct_imports: dict[str, Set[str]] = {} # Removed
149
+ # Relative imports like 'from .models import Pet'
150
+ self.relative_imports: defaultdict[str, set[str]] = defaultdict(set)
151
+ # Plain imports like 'import json'
152
+ self.plain_imports: set[str] = set()
153
+
154
+ # Path information for the current file, used by get_formatted_imports
155
+ self._current_file_module_dot_path: str | None = None
156
+ self._current_file_package_root: str | None = None
157
+ self._current_file_core_pkg_name_for_abs: str | None = None
158
+
159
+ def reset(self) -> None:
160
+ """Reset the collector to its initial empty state."""
161
+ self.imports.clear()
162
+ self.relative_imports.clear()
163
+ self.plain_imports.clear()
164
+ self._current_file_module_dot_path = None
165
+ self._current_file_package_root = None
166
+ self._current_file_core_pkg_name_for_abs = None
167
+
168
+ def set_current_file_context_for_rendering(
169
+ self,
170
+ current_module_dot_path: str | None,
171
+ package_root: str | None,
172
+ core_package_name_for_absolute_treatment: str | None,
173
+ ) -> None:
174
+ """Set the context for the current file, used by get_formatted_imports."""
175
+ self._current_file_module_dot_path = current_module_dot_path
176
+ self._current_file_package_root = package_root
177
+ self._current_file_core_pkg_name_for_abs = core_package_name_for_absolute_treatment
178
+
179
+ def add_import(self, module: str, name: str) -> None:
180
+ """
181
+ Add an import from a specific module.
182
+
183
+ Args:
184
+ module: The module to import from (e.g., "typing")
185
+ name: The name to import (e.g., "List")
186
+ """
187
+ # If module and name are the same, and it's a stdlib module
188
+ # that typically uses plain import style (e.g., "import os").
189
+ if module == name and module in STDLIB_MODULES_PREFER_PLAIN_IMPORT_WHEN_NAME_MATCHES:
190
+ self.add_plain_import(module)
191
+ else:
192
+ if module not in self.imports:
193
+ self.imports[module] = set()
194
+ self.imports[module].add(name)
195
+
196
+ def add_imports(self, module: str, names: List[str]) -> None:
197
+ """
198
+ Add multiple imports from a module.
199
+
200
+ Args:
201
+ module: The module to import from
202
+ names: List of names to import
203
+ """
204
+ for name in names:
205
+ self.add_import(module, name)
206
+
207
+ def add_typing_import(self, name: str) -> None:
208
+ """
209
+ Shortcut for adding typing imports.
210
+
211
+ Args:
212
+ name: The typing name to import (e.g., "List", "Optional")
213
+ """
214
+ self.add_import("typing", name)
215
+
216
+ def add_relative_import(self, module: str, name: str) -> None:
217
+ """
218
+ Add a relative import module and name.
219
+
220
+ Args:
221
+ module: The relative module path (e.g., ".models")
222
+ name: The name to import
223
+ """
224
+ if module not in self.relative_imports:
225
+ self.relative_imports[module] = set()
226
+ self.relative_imports[module].add(name)
227
+
228
+ def add_plain_import(self, module: str) -> None:
229
+ """
230
+ Add a plain import (import x).
231
+
232
+ Args:
233
+ module: The module to import
234
+ """
235
+ self.plain_imports.add(module)
236
+
237
+ def has_import(self, module: str, name: str | None = None) -> bool:
238
+ """Check if a specific module or name within a module is already imported."""
239
+ if name:
240
+ # Check absolute/standard imports
241
+ if module in self.imports and name in self.imports[module]:
242
+ return True
243
+ # Check relative imports
244
+ if module in self.relative_imports and name in self.relative_imports[module]:
245
+ return True
246
+ else:
247
+ # Check plain imports (e.g. "import os" where module="os", name=None)
248
+ if module in self.plain_imports:
249
+ return True
250
+
251
+ return False
252
+
253
+ def get_import_statements(self) -> List[str]:
254
+ """
255
+ Generates a list of import statement strings.
256
+ Order: plain, standard (from x import y), relative (from .x import y).
257
+ Uses path context set by `set_current_file_context_for_rendering`.
258
+ """
259
+ # Use internal state for path context
260
+ current_module_dot_path_to_use = self._current_file_module_dot_path
261
+ package_root_to_use = self._current_file_package_root
262
+ core_package_name_to_use = self._current_file_core_pkg_name_for_abs
263
+
264
+ standard_import_lines: List[str] = []
265
+
266
+ for module_name, names_set in sorted(self.imports.items()):
267
+ names = sorted(list(names_set))
268
+ is_stdlib_module = _is_stdlib(module_name)
269
+
270
+ is_core_module_to_be_absolute = False
271
+ if core_package_name_to_use and (
272
+ module_name.startswith(core_package_name_to_use + ".") or module_name == core_package_name_to_use
273
+ ):
274
+ is_core_module_to_be_absolute = True
275
+
276
+ if is_core_module_to_be_absolute:
277
+ import_statement = f"from {module_name} import {', '.join(names)}"
278
+ standard_import_lines.append(import_statement)
279
+ elif is_stdlib_module:
280
+ import_statement = f"from {module_name} import {', '.join(names)}"
281
+ standard_import_lines.append(import_statement)
282
+ elif (
283
+ current_module_dot_path_to_use
284
+ and package_root_to_use
285
+ and module_name.startswith(package_root_to_use + ".")
286
+ ):
287
+ try:
288
+ relative_module = make_relative_import(current_module_dot_path_to_use, module_name)
289
+ import_statement = f"from {relative_module} import {', '.join(names)}"
290
+ standard_import_lines.append(import_statement)
291
+ except ValueError as e:
292
+ import_statement = f"from {module_name} import {', '.join(names)}"
293
+ standard_import_lines.append(import_statement)
294
+ else:
295
+ import_statement = f"from {module_name} import {', '.join(names)}"
296
+ standard_import_lines.append(import_statement)
297
+
298
+ plain_import_lines: List[str] = []
299
+ for module in sorted(self.plain_imports):
300
+ plain_import_lines.append(f"import {module}")
301
+
302
+ filtered_relative_imports: defaultdict[str, set[str]] = defaultdict(set)
303
+ for module, names_to_import in self.relative_imports.items():
304
+ # A module from self.relative_imports always starts with '.' (e.g., ".models")
305
+ # Include it unless it's a self-import relative to a known current_module_dot_path.
306
+ is_self_import = current_module_dot_path_to_use is not None and module == current_module_dot_path_to_use
307
+ if not is_self_import:
308
+ filtered_relative_imports[module].update(names_to_import)
309
+
310
+ relative_import_lines: List[str] = []
311
+ for module, imported_names in sorted(filtered_relative_imports.items()):
312
+ names_str = ", ".join(sorted(list(imported_names)))
313
+ relative_import_lines.append(f"from {module} import {names_str}")
314
+
315
+ import_lines: List[str] = (
316
+ list(sorted(plain_import_lines)) + list(sorted(standard_import_lines)) + list(sorted(relative_import_lines))
317
+ )
318
+ return import_lines
319
+
320
+ def get_formatted_imports(self) -> str:
321
+ """
322
+ Get all imports as a formatted string.
323
+
324
+ Returns:
325
+ A newline-separated string of import statements
326
+ """
327
+ statements: List[str] = []
328
+
329
+ # Standard library imports first
330
+ stdlib_modules = sorted([m for m in self.imports.keys() if _is_stdlib(m)])
331
+
332
+ for module in stdlib_modules:
333
+ names = sorted(self.imports[module])
334
+ statements.append(f"from {module} import {', '.join(names)}")
335
+
336
+ # Then third-party and app imports
337
+ other_modules = sorted([m for m in self.imports.keys() if not _is_stdlib(m)])
338
+
339
+ if stdlib_modules and other_modules:
340
+ statements.append("") # Add a blank line between stdlib and other imports
341
+
342
+ for module in other_modules:
343
+ names = sorted(self.imports[module])
344
+ statements.append(f"from {module} import {', '.join(names)}")
345
+
346
+ # Then plain imports
347
+ if self.plain_imports:
348
+ if statements: # Add blank line if we have imports already
349
+ statements.append("")
350
+
351
+ for module in sorted(self.plain_imports):
352
+ statements.append(f"import {module}")
353
+
354
+ # Then relative imports
355
+ if self.relative_imports and (stdlib_modules or other_modules or self.plain_imports):
356
+ statements.append("") # Add a blank line before relative imports
357
+
358
+ for module in sorted(self.relative_imports.keys()):
359
+ names = sorted(self.relative_imports[module])
360
+ statements.append(f"from {module} import {', '.join(names)}")
361
+
362
+ return "\n".join(statements)
363
+
364
+ def merge(self, other: "ImportCollector") -> None:
365
+ """
366
+ Merge imports from another ImportCollector instance.
367
+
368
+ This method combines all imports from the other collector into this one.
369
+
370
+ Args:
371
+ other: Another ImportCollector instance to merge imports from
372
+ """
373
+ for module, names in other.imports.items():
374
+ if module not in self.imports:
375
+ self.imports[module] = set()
376
+ self.imports[module].update(names)
377
+ for module, names in other.relative_imports.items():
378
+ if module not in self.relative_imports:
379
+ self.relative_imports[module] = set()
380
+ self.relative_imports[module].update(names)
381
+ for module in other.plain_imports:
382
+ self.plain_imports.add(module)