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.
- pyopenapi_gen/__init__.py +224 -0
- pyopenapi_gen/__main__.py +6 -0
- pyopenapi_gen/cli.py +62 -0
- pyopenapi_gen/context/CLAUDE.md +284 -0
- pyopenapi_gen/context/file_manager.py +52 -0
- pyopenapi_gen/context/import_collector.py +382 -0
- pyopenapi_gen/context/render_context.py +726 -0
- pyopenapi_gen/core/CLAUDE.md +224 -0
- pyopenapi_gen/core/__init__.py +0 -0
- pyopenapi_gen/core/auth/base.py +22 -0
- pyopenapi_gen/core/auth/plugins.py +89 -0
- pyopenapi_gen/core/cattrs_converter.py +810 -0
- pyopenapi_gen/core/exceptions.py +20 -0
- pyopenapi_gen/core/http_status_codes.py +218 -0
- pyopenapi_gen/core/http_transport.py +222 -0
- pyopenapi_gen/core/loader/__init__.py +12 -0
- pyopenapi_gen/core/loader/loader.py +174 -0
- pyopenapi_gen/core/loader/operations/__init__.py +12 -0
- pyopenapi_gen/core/loader/operations/parser.py +161 -0
- pyopenapi_gen/core/loader/operations/post_processor.py +62 -0
- pyopenapi_gen/core/loader/operations/request_body.py +90 -0
- pyopenapi_gen/core/loader/parameters/__init__.py +10 -0
- pyopenapi_gen/core/loader/parameters/parser.py +186 -0
- pyopenapi_gen/core/loader/responses/__init__.py +10 -0
- pyopenapi_gen/core/loader/responses/parser.py +111 -0
- pyopenapi_gen/core/loader/schemas/__init__.py +11 -0
- pyopenapi_gen/core/loader/schemas/extractor.py +275 -0
- pyopenapi_gen/core/pagination.py +64 -0
- pyopenapi_gen/core/parsing/__init__.py +13 -0
- pyopenapi_gen/core/parsing/common/__init__.py +1 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/__init__.py +9 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/cyclic_properties.py +66 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/direct_cycle.py +33 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/existing_schema.py +22 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +54 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +52 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +50 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +51 -0
- pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +86 -0
- pyopenapi_gen/core/parsing/common/type_parser.py +73 -0
- pyopenapi_gen/core/parsing/context.py +187 -0
- pyopenapi_gen/core/parsing/cycle_helpers.py +126 -0
- pyopenapi_gen/core/parsing/keywords/__init__.py +1 -0
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +81 -0
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +84 -0
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +72 -0
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +77 -0
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +98 -0
- pyopenapi_gen/core/parsing/schema_finalizer.py +169 -0
- pyopenapi_gen/core/parsing/schema_parser.py +804 -0
- pyopenapi_gen/core/parsing/transformers/__init__.py +0 -0
- pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +285 -0
- pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +120 -0
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +293 -0
- pyopenapi_gen/core/postprocess_manager.py +260 -0
- pyopenapi_gen/core/spec_fetcher.py +148 -0
- pyopenapi_gen/core/streaming_helpers.py +84 -0
- pyopenapi_gen/core/telemetry.py +69 -0
- pyopenapi_gen/core/utils.py +456 -0
- pyopenapi_gen/core/warning_collector.py +83 -0
- pyopenapi_gen/core/writers/code_writer.py +135 -0
- pyopenapi_gen/core/writers/documentation_writer.py +222 -0
- pyopenapi_gen/core/writers/line_writer.py +217 -0
- pyopenapi_gen/core/writers/python_construct_renderer.py +321 -0
- pyopenapi_gen/core_package_template/README.md +21 -0
- pyopenapi_gen/emit/models_emitter.py +143 -0
- pyopenapi_gen/emitters/CLAUDE.md +286 -0
- pyopenapi_gen/emitters/client_emitter.py +51 -0
- pyopenapi_gen/emitters/core_emitter.py +181 -0
- pyopenapi_gen/emitters/docs_emitter.py +44 -0
- pyopenapi_gen/emitters/endpoints_emitter.py +247 -0
- pyopenapi_gen/emitters/exceptions_emitter.py +187 -0
- pyopenapi_gen/emitters/mocks_emitter.py +185 -0
- pyopenapi_gen/emitters/models_emitter.py +426 -0
- pyopenapi_gen/generator/CLAUDE.md +352 -0
- pyopenapi_gen/generator/client_generator.py +567 -0
- pyopenapi_gen/generator/exceptions.py +7 -0
- pyopenapi_gen/helpers/CLAUDE.md +325 -0
- pyopenapi_gen/helpers/__init__.py +1 -0
- pyopenapi_gen/helpers/endpoint_utils.py +532 -0
- pyopenapi_gen/helpers/type_cleaner.py +334 -0
- pyopenapi_gen/helpers/type_helper.py +112 -0
- pyopenapi_gen/helpers/type_resolution/__init__.py +1 -0
- pyopenapi_gen/helpers/type_resolution/array_resolver.py +57 -0
- pyopenapi_gen/helpers/type_resolution/composition_resolver.py +79 -0
- pyopenapi_gen/helpers/type_resolution/finalizer.py +105 -0
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +172 -0
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +216 -0
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +109 -0
- pyopenapi_gen/helpers/type_resolution/resolver.py +47 -0
- pyopenapi_gen/helpers/url_utils.py +14 -0
- pyopenapi_gen/http_types.py +20 -0
- pyopenapi_gen/ir.py +165 -0
- pyopenapi_gen/py.typed +1 -0
- pyopenapi_gen/types/CLAUDE.md +140 -0
- pyopenapi_gen/types/__init__.py +11 -0
- pyopenapi_gen/types/contracts/__init__.py +13 -0
- pyopenapi_gen/types/contracts/protocols.py +106 -0
- pyopenapi_gen/types/contracts/types.py +28 -0
- pyopenapi_gen/types/resolvers/__init__.py +7 -0
- pyopenapi_gen/types/resolvers/reference_resolver.py +71 -0
- pyopenapi_gen/types/resolvers/response_resolver.py +177 -0
- pyopenapi_gen/types/resolvers/schema_resolver.py +498 -0
- pyopenapi_gen/types/services/__init__.py +5 -0
- pyopenapi_gen/types/services/type_service.py +165 -0
- pyopenapi_gen/types/strategies/__init__.py +5 -0
- pyopenapi_gen/types/strategies/response_strategy.py +310 -0
- pyopenapi_gen/visit/CLAUDE.md +272 -0
- pyopenapi_gen/visit/client_visitor.py +477 -0
- pyopenapi_gen/visit/docs_visitor.py +38 -0
- pyopenapi_gen/visit/endpoint/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/endpoint_visitor.py +292 -0
- pyopenapi_gen/visit/endpoint/generators/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +123 -0
- pyopenapi_gen/visit/endpoint/generators/endpoint_method_generator.py +222 -0
- pyopenapi_gen/visit/endpoint/generators/mock_generator.py +140 -0
- pyopenapi_gen/visit/endpoint/generators/overload_generator.py +252 -0
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +103 -0
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +705 -0
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +83 -0
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +207 -0
- pyopenapi_gen/visit/endpoint/processors/__init__.py +1 -0
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +78 -0
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +171 -0
- pyopenapi_gen/visit/exception_visitor.py +90 -0
- pyopenapi_gen/visit/model/__init__.py +0 -0
- pyopenapi_gen/visit/model/alias_generator.py +93 -0
- pyopenapi_gen/visit/model/dataclass_generator.py +553 -0
- pyopenapi_gen/visit/model/enum_generator.py +212 -0
- pyopenapi_gen/visit/model/model_visitor.py +198 -0
- pyopenapi_gen/visit/visitor.py +97 -0
- pyopenapi_gen-2.7.2.dist-info/METADATA +1169 -0
- pyopenapi_gen-2.7.2.dist-info/RECORD +137 -0
- pyopenapi_gen-2.7.2.dist-info/WHEEL +4 -0
- pyopenapi_gen-2.7.2.dist-info/entry_points.txt +2 -0
- 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)
|