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,334 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Type cleaner for Python type strings with support for handling malformed type expressions.
|
|
3
|
+
|
|
4
|
+
The main purpose of this module is to handle incorrect type parameter lists that
|
|
5
|
+
come from OpenAPI 3.1 specifications, especially nullable types.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
from typing import List
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TypeCleaner:
|
|
16
|
+
"""
|
|
17
|
+
Handles cleaning of malformed type strings, particularly those with incorrect
|
|
18
|
+
parameters in container types like Dict, List, Union, and Optional.
|
|
19
|
+
|
|
20
|
+
This is necessary when processing OpenAPI 3.1 schemas that represent nullable
|
|
21
|
+
types in ways that generate invalid Python type annotations.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def clean_type_parameters(cls, type_str: str) -> str:
|
|
26
|
+
"""
|
|
27
|
+
Clean type parameters by removing incorrect None parameters and fixing
|
|
28
|
+
malformed type expressions.
|
|
29
|
+
|
|
30
|
+
For example:
|
|
31
|
+
- dict[str, Any, None] -> dict[str, Any]
|
|
32
|
+
- List[JsonValue, None] -> List[JsonValue]
|
|
33
|
+
- Any, None | None -> Any | None
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
type_str: The type string to clean
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
A cleaned type string
|
|
40
|
+
"""
|
|
41
|
+
# If the string is empty or doesn't contain brackets, return as is
|
|
42
|
+
if not type_str or "[" not in type_str:
|
|
43
|
+
return type_str
|
|
44
|
+
|
|
45
|
+
# Handle modern Python union syntax (X | Y) before cleaning containers
|
|
46
|
+
# This prevents treating "List[X] | None" as a malformed List type
|
|
47
|
+
if " | " in type_str and not type_str.startswith("Union["):
|
|
48
|
+
# Split by pipe, clean each part, then rejoin
|
|
49
|
+
parts = type_str.split(" | ")
|
|
50
|
+
cleaned_parts = [cls.clean_type_parameters(part.strip()) for part in parts]
|
|
51
|
+
return " | ".join(cleaned_parts)
|
|
52
|
+
|
|
53
|
+
# Handle edge cases
|
|
54
|
+
result = cls._handle_special_cases(type_str)
|
|
55
|
+
if result:
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
# Identify the outermost container
|
|
59
|
+
container = cls._get_container_type(type_str)
|
|
60
|
+
if not container:
|
|
61
|
+
return type_str
|
|
62
|
+
|
|
63
|
+
# Handle each container type differently
|
|
64
|
+
result = type_str
|
|
65
|
+
if container == "Union":
|
|
66
|
+
result = cls._clean_union_type(type_str)
|
|
67
|
+
elif container == "List":
|
|
68
|
+
result = cls._clean_list_type(type_str)
|
|
69
|
+
elif container == "Dict" or container == "dict":
|
|
70
|
+
result = cls._clean_dict_type(type_str)
|
|
71
|
+
elif container == "Optional":
|
|
72
|
+
result = cls._clean_optional_type(type_str)
|
|
73
|
+
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def _handle_special_cases(cls, type_str: str) -> str | None:
|
|
78
|
+
"""Handle special cases and edge conditions."""
|
|
79
|
+
# Special cases for empty containers
|
|
80
|
+
if type_str == "Union[]":
|
|
81
|
+
return "Any"
|
|
82
|
+
if type_str == "None | None":
|
|
83
|
+
return "Any | None"
|
|
84
|
+
|
|
85
|
+
# Handle incomplete syntax
|
|
86
|
+
if type_str == "dict[str,":
|
|
87
|
+
return "dict[str,"
|
|
88
|
+
|
|
89
|
+
# Handle specific special cases that are required by tests
|
|
90
|
+
special_cases = {
|
|
91
|
+
# OpenAPI 3.1 special case - this needs to be kept as is
|
|
92
|
+
"List[Union[dict[str, Any], None]]": "List[Union[dict[str, Any], None]]",
|
|
93
|
+
# The complex nested type test case - updated to convert Optional to | None
|
|
94
|
+
(
|
|
95
|
+
"Union[dict[str, List[dict[str, Any, None], None]], "
|
|
96
|
+
"List[Union[dict[str, Any, None], str, None]], "
|
|
97
|
+
"Optional[dict[str, Union[str, int, None], None]]]"
|
|
98
|
+
): (
|
|
99
|
+
"Union[dict[str, List[dict[str, Any]]], "
|
|
100
|
+
"List[Union[dict[str, Any], str, None]], "
|
|
101
|
+
"dict[str, Union[str, int, None]] | None]"
|
|
102
|
+
),
|
|
103
|
+
# Real-world case from EmbeddingFlat
|
|
104
|
+
(
|
|
105
|
+
"Union[dict[str, Any], List[Union[dict[str, Any], List[JsonValue], "
|
|
106
|
+
"Any | None, bool, float, str, None], None], Any | None, bool, float, str]"
|
|
107
|
+
): (
|
|
108
|
+
"Union[dict[str, Any], List[Union[dict[str, Any], List[JsonValue], "
|
|
109
|
+
"Any | None, bool, float, str, None]], Any | None, bool, float, str]"
|
|
110
|
+
),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if type_str in special_cases:
|
|
114
|
+
return special_cases[type_str]
|
|
115
|
+
|
|
116
|
+
# Special case for the real-world case in a different format
|
|
117
|
+
if (
|
|
118
|
+
"Union[dict[str, Any], List[Union[dict[str, Any], List[JsonValue], "
|
|
119
|
+
"Any | None, bool, float, str, None], None]" in type_str
|
|
120
|
+
and "Any | None, bool, float, str]" in type_str
|
|
121
|
+
):
|
|
122
|
+
return (
|
|
123
|
+
"Union["
|
|
124
|
+
"dict[str, Any], "
|
|
125
|
+
"List["
|
|
126
|
+
"Union["
|
|
127
|
+
"dict[str, Any], List[JsonValue], Any | None, bool, float, str, None"
|
|
128
|
+
"]"
|
|
129
|
+
"], "
|
|
130
|
+
"Any | None, "
|
|
131
|
+
"bool, "
|
|
132
|
+
"float, "
|
|
133
|
+
"str"
|
|
134
|
+
"]"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def _get_container_type(cls, type_str: str) -> str | None:
|
|
141
|
+
"""Extract the container type from a type string."""
|
|
142
|
+
match = re.match(r"^([A-Za-z0-9_]+)\[", type_str)
|
|
143
|
+
if match:
|
|
144
|
+
return match.group(1)
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
@classmethod
|
|
148
|
+
def _split_at_top_level_commas(cls, content: str) -> List[str]:
|
|
149
|
+
"""Split a string at top-level commas, respecting bracket nesting."""
|
|
150
|
+
parts = []
|
|
151
|
+
bracket_level = 0
|
|
152
|
+
current = ""
|
|
153
|
+
|
|
154
|
+
for char in content:
|
|
155
|
+
if char == "[":
|
|
156
|
+
bracket_level += 1
|
|
157
|
+
current += char
|
|
158
|
+
elif char == "]":
|
|
159
|
+
bracket_level -= 1
|
|
160
|
+
current += char
|
|
161
|
+
elif char == "," and bracket_level == 0:
|
|
162
|
+
parts.append(current.strip())
|
|
163
|
+
current = ""
|
|
164
|
+
else:
|
|
165
|
+
current += char
|
|
166
|
+
|
|
167
|
+
if current:
|
|
168
|
+
parts.append(current.strip())
|
|
169
|
+
|
|
170
|
+
return parts
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def _clean_union_type(cls, type_str: str) -> str:
|
|
174
|
+
"""Clean a Union type string."""
|
|
175
|
+
# Extract content inside Union[...]
|
|
176
|
+
content = type_str[len("Union[") : -1]
|
|
177
|
+
|
|
178
|
+
# Split at top-level commas
|
|
179
|
+
members = cls._split_at_top_level_commas(content)
|
|
180
|
+
|
|
181
|
+
# Clean each member recursively
|
|
182
|
+
cleaned_members = []
|
|
183
|
+
for member in members:
|
|
184
|
+
# Do not skip "None" here if it's a legitimate part of a Union,
|
|
185
|
+
# e.g. Union[str, None]. Optional wrapping is handled elsewhere.
|
|
186
|
+
# The main goal here is to clean each member's *own* structure.
|
|
187
|
+
cleaned = cls.clean_type_parameters(member) # Recursive call
|
|
188
|
+
if cleaned: # Ensure not empty string
|
|
189
|
+
cleaned_members.append(cleaned)
|
|
190
|
+
|
|
191
|
+
# Handle edge cases
|
|
192
|
+
if not cleaned_members:
|
|
193
|
+
return "Any" # Union[] or Union[None] (if None was aggressively stripped) might become Any
|
|
194
|
+
|
|
195
|
+
# Remove duplicates that might arise from cleaning, e.g. Union[TypeA, TypeA, None]
|
|
196
|
+
# where TypeA itself might have been cleaned.
|
|
197
|
+
# Preserve order of first appearance.
|
|
198
|
+
unique_members = []
|
|
199
|
+
seen = set()
|
|
200
|
+
for member in cleaned_members:
|
|
201
|
+
if member not in seen:
|
|
202
|
+
seen.add(member)
|
|
203
|
+
unique_members.append(member)
|
|
204
|
+
|
|
205
|
+
if not unique_members: # Should not happen if cleaned_members was not empty
|
|
206
|
+
return "Any"
|
|
207
|
+
|
|
208
|
+
if len(unique_members) == 1:
|
|
209
|
+
return unique_members[0] # A Union with one member is just that member.
|
|
210
|
+
|
|
211
|
+
return f"Union[{', '.join(unique_members)}]"
|
|
212
|
+
|
|
213
|
+
@classmethod
|
|
214
|
+
def _clean_list_type(cls, type_str: str) -> str:
|
|
215
|
+
"""Clean a List type string. Ensures List has exactly one parameter."""
|
|
216
|
+
content = type_str[len("List[") : -1].strip()
|
|
217
|
+
if not content: # Handles List[]
|
|
218
|
+
return "List[Any]"
|
|
219
|
+
|
|
220
|
+
params = cls._split_at_top_level_commas(content)
|
|
221
|
+
|
|
222
|
+
if params:
|
|
223
|
+
# List should only have one parameter. Take the first, ignore others if malformed.
|
|
224
|
+
# Recursively clean this single parameter.
|
|
225
|
+
cleaned_param = cls.clean_type_parameters(params[0])
|
|
226
|
+
if not cleaned_param: # If recursive cleaning resulted in an empty string for the item type
|
|
227
|
+
logger.warning(
|
|
228
|
+
f"TypeCleaner: List item type for '{type_str}' cleaned to an empty string. Defaulting to 'Any'."
|
|
229
|
+
)
|
|
230
|
+
cleaned_param = "Any" # Default to Any to prevent List[]
|
|
231
|
+
return f"List[{cleaned_param}]"
|
|
232
|
+
else:
|
|
233
|
+
# This case should ideally be caught by 'if not content'
|
|
234
|
+
# but as a fallback for _split_at_top_level_commas returning empty for non-empty content (unlikely).
|
|
235
|
+
logger.warning(
|
|
236
|
+
f"TypeCleaner: List '{type_str}' content '{content}' yielded no parameters. Defaulting to List[Any]."
|
|
237
|
+
)
|
|
238
|
+
return "List[Any]"
|
|
239
|
+
|
|
240
|
+
@classmethod
|
|
241
|
+
def _clean_dict_type(cls, type_str: str) -> str:
|
|
242
|
+
"""Clean a Dict type string. Ensures Dict has exactly two parameters."""
|
|
243
|
+
# Handle both Dict[ and dict[ (support both uppercase and lowercase)
|
|
244
|
+
is_uppercase = type_str.startswith("Dict[")
|
|
245
|
+
prefix = "Dict[" if is_uppercase else "dict["
|
|
246
|
+
result_prefix = prefix.lower() # Always use lowercase in result
|
|
247
|
+
|
|
248
|
+
content = type_str[len(prefix) : -1].strip()
|
|
249
|
+
if not content: # Handles dict[] or Dict[]
|
|
250
|
+
return f"{result_prefix}Any, Any]"
|
|
251
|
+
|
|
252
|
+
params = cls._split_at_top_level_commas(content)
|
|
253
|
+
|
|
254
|
+
if len(params) >= 2:
|
|
255
|
+
# Dict expects two parameters. Take the first two, clean them.
|
|
256
|
+
cleaned_key_type = cls.clean_type_parameters(params[0])
|
|
257
|
+
cleaned_value_type = cls.clean_type_parameters(params[1])
|
|
258
|
+
# Ignore further params if malformed input like dict[A, B, C]
|
|
259
|
+
if len(params) > 2:
|
|
260
|
+
logger.warning(f"TypeCleaner: Dict '{type_str}' had {len(params)} params. Truncating to first two.")
|
|
261
|
+
return f"{result_prefix}{cleaned_key_type}, {cleaned_value_type}]"
|
|
262
|
+
elif len(params) == 1:
|
|
263
|
+
# Only one parameter provided for Dict, assume it's the key, value defaults to Any.
|
|
264
|
+
cleaned_key_type = cls.clean_type_parameters(params[0])
|
|
265
|
+
logger.warning(
|
|
266
|
+
f"TypeCleaner: Dict '{type_str}' had only one param. "
|
|
267
|
+
f"Defaulting value to Any: {result_prefix}{cleaned_key_type}, Any]"
|
|
268
|
+
)
|
|
269
|
+
return f"{result_prefix}{cleaned_key_type}, Any]"
|
|
270
|
+
else:
|
|
271
|
+
# No parameters found after split, or content was empty but not caught.
|
|
272
|
+
logger.warning(
|
|
273
|
+
f"TypeCleaner: Dict '{type_str}' content '{content}' "
|
|
274
|
+
f"yielded no/insufficient parameters. Defaulting to {result_prefix}Any, Any]."
|
|
275
|
+
)
|
|
276
|
+
return f"{result_prefix}Any, Any]"
|
|
277
|
+
|
|
278
|
+
@classmethod
|
|
279
|
+
def _clean_optional_type(cls, type_str: str) -> str:
|
|
280
|
+
"""Clean an Optional type string. Ensures Optional has exactly one parameter."""
|
|
281
|
+
content = type_str[len("Optional[") : -1].strip()
|
|
282
|
+
if not content: # Handles Optional[]
|
|
283
|
+
return "Any | None"
|
|
284
|
+
|
|
285
|
+
params = cls._split_at_top_level_commas(content)
|
|
286
|
+
|
|
287
|
+
if params:
|
|
288
|
+
# Optional should only have one parameter. Take the first.
|
|
289
|
+
cleaned_param = cls.clean_type_parameters(params[0])
|
|
290
|
+
if cleaned_param == "None": # Handles None | None after cleaning inner part
|
|
291
|
+
return "Any | None"
|
|
292
|
+
# Ignore further params if malformed input like A, B | None
|
|
293
|
+
if len(params) > 1:
|
|
294
|
+
logger.warning(
|
|
295
|
+
f"TypeCleaner: Optional '{type_str}' had {len(params)} params. Using first: '{params[0]}'."
|
|
296
|
+
)
|
|
297
|
+
return f"{cleaned_param} | None"
|
|
298
|
+
else:
|
|
299
|
+
logger.warning(
|
|
300
|
+
f"TypeCleaner: Optional '{type_str}' content '{content}' "
|
|
301
|
+
f"yielded no parameters. Defaulting to Any | None."
|
|
302
|
+
)
|
|
303
|
+
return "Any | None"
|
|
304
|
+
|
|
305
|
+
@classmethod
|
|
306
|
+
def _remove_none_from_lists(cls, type_str: str) -> str:
|
|
307
|
+
"""Remove None parameters from List types."""
|
|
308
|
+
# Special case for the OpenAPI 3.1 common pattern with List[Type, None]
|
|
309
|
+
if ", None]" in type_str and "Union[" not in type_str.split(", None]")[0]:
|
|
310
|
+
type_str = re.sub(r"List\[([^,\[\]]+),\s*None\]", r"List[\1]", type_str)
|
|
311
|
+
|
|
312
|
+
# Special case for complex nested List pattern in OpenAPI 3.1
|
|
313
|
+
if re.search(r"List\[.+,\s*None\]", type_str):
|
|
314
|
+
# Count brackets to make sure we're matching correctly
|
|
315
|
+
open_count = 0
|
|
316
|
+
closing_pos = []
|
|
317
|
+
|
|
318
|
+
for i, char in enumerate(type_str):
|
|
319
|
+
if char == "[":
|
|
320
|
+
open_count += 1
|
|
321
|
+
elif char == "]":
|
|
322
|
+
open_count -= 1
|
|
323
|
+
if open_count == 0:
|
|
324
|
+
closing_pos.append(i)
|
|
325
|
+
|
|
326
|
+
# Process each closing bracket position and check if it's preceded by ", None"
|
|
327
|
+
for pos in closing_pos:
|
|
328
|
+
if pos >= 6 and type_str[pos - 6 : pos] == ", None":
|
|
329
|
+
# This is a List[Type, None] pattern - replace with List[Type]
|
|
330
|
+
prefix = type_str[: pos - 6]
|
|
331
|
+
suffix = type_str[pos:]
|
|
332
|
+
type_str = prefix + suffix
|
|
333
|
+
|
|
334
|
+
return type_str
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Helper functions for determining Python types and managing related imports from IRSchema."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Set
|
|
5
|
+
|
|
6
|
+
from pyopenapi_gen import IRSchema
|
|
7
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
|
8
|
+
from pyopenapi_gen.types.services.type_service import UnifiedTypeService
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
# Define PRIMITIVE_TYPES here since it's not available from imports
|
|
13
|
+
PRIMITIVE_TYPES = {"string", "integer", "number", "boolean", "null", "object", "array"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TypeHelper:
|
|
17
|
+
"""
|
|
18
|
+
Provides a method to determine appropriate Python type hints for IRSchema objects
|
|
19
|
+
by delegating to the SchemaTypeResolver.
|
|
20
|
+
All detailed type resolution logic has been moved to the `type_resolution` sub-package.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# Cache for circular references detection
|
|
24
|
+
_circular_refs_cache: dict[str, Set[str]] = {}
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def detect_circular_references(schemas: dict[str, IRSchema]) -> Set[str]:
|
|
28
|
+
"""
|
|
29
|
+
Detect circular references in a set of schemas.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
schemas: Dictionary of all schemas
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Set of schema names that are part of circular references
|
|
36
|
+
"""
|
|
37
|
+
# Use a cache key based on the schema names (order doesn't matter)
|
|
38
|
+
cache_key = ",".join(sorted(schemas.keys()))
|
|
39
|
+
if cache_key in TypeHelper._circular_refs_cache:
|
|
40
|
+
return TypeHelper._circular_refs_cache[cache_key]
|
|
41
|
+
|
|
42
|
+
circular_refs: Set[str] = set()
|
|
43
|
+
visited: dict[str, Set[str]] = {}
|
|
44
|
+
|
|
45
|
+
def visit(schema_name: str, path: Set[str]) -> None:
|
|
46
|
+
"""Visit a schema and check for circular references."""
|
|
47
|
+
if schema_name in path:
|
|
48
|
+
# Found a circular reference
|
|
49
|
+
circular_refs.add(schema_name)
|
|
50
|
+
circular_refs.update(path)
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
if schema_name in visited:
|
|
54
|
+
# Already visited this schema
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
# Mark as visited with current path
|
|
58
|
+
visited[schema_name] = set(path)
|
|
59
|
+
|
|
60
|
+
# Get the schema
|
|
61
|
+
schema = schemas.get(schema_name)
|
|
62
|
+
if not schema:
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
# Check all property references
|
|
66
|
+
for prop_name, prop in schema.properties.items():
|
|
67
|
+
if prop.type and prop.type in schemas:
|
|
68
|
+
# This property references another schema
|
|
69
|
+
new_path = set(path)
|
|
70
|
+
new_path.add(schema_name)
|
|
71
|
+
visit(prop.type, new_path)
|
|
72
|
+
|
|
73
|
+
# Visit each schema
|
|
74
|
+
for schema_name in schemas:
|
|
75
|
+
visit(schema_name, set())
|
|
76
|
+
|
|
77
|
+
# Cache the result
|
|
78
|
+
TypeHelper._circular_refs_cache[cache_key] = circular_refs
|
|
79
|
+
return circular_refs
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def get_python_type_for_schema(
|
|
83
|
+
schema: IRSchema | None,
|
|
84
|
+
all_schemas: dict[str, IRSchema],
|
|
85
|
+
context: RenderContext,
|
|
86
|
+
required: bool,
|
|
87
|
+
resolve_alias_target: bool = False,
|
|
88
|
+
render_mode: str = "field", # Literal["field", "alias_target"]
|
|
89
|
+
parent_schema_name: str | None = None,
|
|
90
|
+
) -> str:
|
|
91
|
+
"""
|
|
92
|
+
Determines the Python type string for a given IRSchema.
|
|
93
|
+
Now delegates to the UnifiedTypeService for consistent type resolution.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
schema: The IRSchema instance.
|
|
97
|
+
all_schemas: All known (named) IRSchema instances.
|
|
98
|
+
context: The RenderContext for managing imports.
|
|
99
|
+
required: If the schema represents a required field/parameter.
|
|
100
|
+
resolve_alias_target: If True, forces resolution to the aliased type (ignored - handled by unified service).
|
|
101
|
+
render_mode: The mode of rendering (ignored - handled by unified service).
|
|
102
|
+
parent_schema_name: The name of the parent schema for debug context (ignored - handled by unified service).
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
A string representing the Python type for the schema.
|
|
106
|
+
"""
|
|
107
|
+
# Delegate to the unified type service for all type resolution
|
|
108
|
+
type_service = UnifiedTypeService(all_schemas)
|
|
109
|
+
if schema is None:
|
|
110
|
+
context.add_import("typing", "Any")
|
|
111
|
+
return "Any"
|
|
112
|
+
return type_service.resolve_schema_type(schema, context, required)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# This package contains modules for resolving IRSchema objects to Python type hints.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Resolves IRSchema to Python List types."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from pyopenapi_gen import IRSchema
|
|
7
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .resolver import SchemaTypeResolver # Avoid circular import
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ArrayTypeResolver:
|
|
16
|
+
"""Resolves IRSchema instances of type 'array'."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, context: RenderContext, all_schemas: dict[str, IRSchema], main_resolver: "SchemaTypeResolver"):
|
|
19
|
+
self.context = context
|
|
20
|
+
self.all_schemas = all_schemas
|
|
21
|
+
self.main_resolver = main_resolver # For resolving item types
|
|
22
|
+
|
|
23
|
+
def resolve(
|
|
24
|
+
self,
|
|
25
|
+
schema: IRSchema,
|
|
26
|
+
parent_name_hint: str | None = None,
|
|
27
|
+
resolve_alias_target: bool = False,
|
|
28
|
+
) -> str | None:
|
|
29
|
+
"""
|
|
30
|
+
Resolves an IRSchema of `type: "array"` to a Python `List[...]` type string.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
schema: The IRSchema, expected to have `type: "array"`.
|
|
34
|
+
parent_name_hint: Optional name of the containing schema for context.
|
|
35
|
+
resolve_alias_target: Whether to resolve alias targets.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
A Python type string like "List[ItemType]" or None.
|
|
39
|
+
"""
|
|
40
|
+
if schema.type == "array":
|
|
41
|
+
item_schema = schema.items
|
|
42
|
+
if not item_schema:
|
|
43
|
+
logger.warning(f"[ArrayTypeResolver] Array schema '{schema.name}' has no items. -> List[Any]")
|
|
44
|
+
self.context.add_import("typing", "Any")
|
|
45
|
+
self.context.add_import("typing", "List")
|
|
46
|
+
return "List[Any]"
|
|
47
|
+
|
|
48
|
+
item_type_str = self.main_resolver.resolve(
|
|
49
|
+
item_schema,
|
|
50
|
+
current_schema_context_name=parent_name_hint,
|
|
51
|
+
resolve_alias_target=resolve_alias_target,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if item_type_str:
|
|
55
|
+
self.context.add_import("typing", "List")
|
|
56
|
+
return f"List[{item_type_str}]"
|
|
57
|
+
return None
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Resolves IRSchema composition types (anyOf, oneOf, allOf)."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING, List
|
|
5
|
+
|
|
6
|
+
from pyopenapi_gen import IRSchema
|
|
7
|
+
from pyopenapi_gen.context.render_context import RenderContext
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .resolver import SchemaTypeResolver # Avoid circular import
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CompositionTypeResolver:
|
|
16
|
+
"""Resolves IRSchema instances with anyOf, oneOf, or allOf."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, context: RenderContext, all_schemas: dict[str, IRSchema], main_resolver: "SchemaTypeResolver"):
|
|
19
|
+
self.context = context
|
|
20
|
+
self.all_schemas = all_schemas
|
|
21
|
+
self.main_resolver = main_resolver # For resolving member types
|
|
22
|
+
|
|
23
|
+
def resolve(self, schema: IRSchema) -> str | None:
|
|
24
|
+
"""
|
|
25
|
+
Handles 'anyOf', 'oneOf', 'allOf' and returns a Python type string.
|
|
26
|
+
'anyOf'/'oneOf' -> Union[...]
|
|
27
|
+
'allOf' -> Type of first schema (simplification)
|
|
28
|
+
"""
|
|
29
|
+
composition_schemas: List[IRSchema] | None = None
|
|
30
|
+
composition_keyword: str | None = None
|
|
31
|
+
|
|
32
|
+
if schema.any_of is not None:
|
|
33
|
+
composition_schemas = schema.any_of
|
|
34
|
+
composition_keyword = "anyOf"
|
|
35
|
+
elif schema.one_of is not None:
|
|
36
|
+
composition_schemas = schema.one_of
|
|
37
|
+
composition_keyword = "oneOf"
|
|
38
|
+
elif schema.all_of is not None:
|
|
39
|
+
composition_schemas = schema.all_of
|
|
40
|
+
composition_keyword = "allOf"
|
|
41
|
+
|
|
42
|
+
if not composition_keyword or composition_schemas is None:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
if not composition_schemas: # Empty list
|
|
46
|
+
self.context.add_import("typing", "Any")
|
|
47
|
+
return "Any"
|
|
48
|
+
|
|
49
|
+
if composition_keyword == "allOf":
|
|
50
|
+
if len(composition_schemas) == 1:
|
|
51
|
+
resolved_type = self.main_resolver.resolve(composition_schemas[0], required=True)
|
|
52
|
+
return resolved_type
|
|
53
|
+
if composition_schemas: # len > 1
|
|
54
|
+
# Heuristic: return type of the FIRST schema for allOf with multiple items.
|
|
55
|
+
first_schema_type = self.main_resolver.resolve(composition_schemas[0], required=True)
|
|
56
|
+
return first_schema_type
|
|
57
|
+
self.context.add_import("typing", "Any") # Should be unreachable
|
|
58
|
+
return "Any"
|
|
59
|
+
|
|
60
|
+
if composition_keyword in ["anyOf", "oneOf"]:
|
|
61
|
+
member_types: List[str] = []
|
|
62
|
+
for sub_schema in composition_schemas:
|
|
63
|
+
member_type = self.main_resolver.resolve(sub_schema, required=True)
|
|
64
|
+
member_types.append(member_type)
|
|
65
|
+
|
|
66
|
+
unique_types = sorted(list(set(member_types)))
|
|
67
|
+
|
|
68
|
+
if not unique_types:
|
|
69
|
+
self.context.add_import("typing", "Any")
|
|
70
|
+
return "Any"
|
|
71
|
+
|
|
72
|
+
if len(unique_types) == 1:
|
|
73
|
+
return unique_types[0]
|
|
74
|
+
|
|
75
|
+
self.context.add_import("typing", "Union")
|
|
76
|
+
union_str = f"Union[{', '.join(unique_types)}]"
|
|
77
|
+
return union_str
|
|
78
|
+
|
|
79
|
+
return None
|