pyopenapi-gen 0.14.0__py3-none-any.whl → 0.14.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/cli.py +3 -3
- pyopenapi_gen/context/import_collector.py +10 -10
- pyopenapi_gen/context/render_context.py +13 -13
- pyopenapi_gen/core/auth/plugins.py +7 -7
- pyopenapi_gen/core/http_status_codes.py +2 -4
- pyopenapi_gen/core/http_transport.py +19 -19
- pyopenapi_gen/core/loader/operations/parser.py +2 -2
- pyopenapi_gen/core/loader/operations/request_body.py +3 -3
- pyopenapi_gen/core/loader/parameters/parser.py +3 -3
- pyopenapi_gen/core/loader/responses/parser.py +2 -2
- pyopenapi_gen/core/loader/schemas/extractor.py +4 -4
- pyopenapi_gen/core/pagination.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +2 -2
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +2 -2
- pyopenapi_gen/core/parsing/common/type_parser.py +2 -3
- pyopenapi_gen/core/parsing/context.py +10 -10
- pyopenapi_gen/core/parsing/cycle_helpers.py +5 -2
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +5 -5
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +4 -4
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +4 -4
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +4 -4
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +5 -5
- pyopenapi_gen/core/parsing/schema_finalizer.py +15 -15
- pyopenapi_gen/core/parsing/schema_parser.py +44 -25
- pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +4 -4
- pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +7 -4
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +10 -10
- pyopenapi_gen/core/schemas.py +10 -10
- pyopenapi_gen/core/streaming_helpers.py +5 -7
- pyopenapi_gen/core/telemetry.py +4 -4
- pyopenapi_gen/core/utils.py +7 -7
- pyopenapi_gen/core/writers/code_writer.py +2 -2
- pyopenapi_gen/core/writers/documentation_writer.py +18 -18
- pyopenapi_gen/core/writers/line_writer.py +3 -3
- pyopenapi_gen/core/writers/python_construct_renderer.py +10 -10
- pyopenapi_gen/emit/models_emitter.py +2 -2
- pyopenapi_gen/emitters/core_emitter.py +3 -5
- pyopenapi_gen/emitters/endpoints_emitter.py +24 -16
- pyopenapi_gen/emitters/exceptions_emitter.py +4 -3
- pyopenapi_gen/emitters/models_emitter.py +6 -6
- pyopenapi_gen/generator/client_generator.py +6 -6
- pyopenapi_gen/helpers/endpoint_utils.py +16 -18
- pyopenapi_gen/helpers/type_cleaner.py +66 -53
- pyopenapi_gen/helpers/type_helper.py +7 -7
- pyopenapi_gen/helpers/type_resolution/array_resolver.py +4 -4
- pyopenapi_gen/helpers/type_resolution/composition_resolver.py +5 -5
- pyopenapi_gen/helpers/type_resolution/finalizer.py +38 -22
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +4 -5
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +11 -11
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +1 -2
- pyopenapi_gen/helpers/type_resolution/resolver.py +2 -3
- pyopenapi_gen/ir.py +32 -34
- pyopenapi_gen/types/contracts/protocols.py +5 -5
- pyopenapi_gen/types/contracts/types.py +2 -3
- pyopenapi_gen/types/resolvers/reference_resolver.py +4 -4
- pyopenapi_gen/types/resolvers/response_resolver.py +6 -4
- pyopenapi_gen/types/resolvers/schema_resolver.py +32 -16
- pyopenapi_gen/types/services/type_service.py +55 -9
- pyopenapi_gen/types/strategies/response_strategy.py +6 -7
- pyopenapi_gen/visit/client_visitor.py +5 -7
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +7 -7
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +5 -5
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +38 -17
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +4 -4
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +17 -17
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +8 -8
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +13 -13
- pyopenapi_gen/visit/model/alias_generator.py +1 -4
- pyopenapi_gen/visit/model/dataclass_generator.py +139 -10
- pyopenapi_gen/visit/model/model_visitor.py +2 -3
- pyopenapi_gen/visit/visitor.py +3 -3
- {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.2.dist-info}/METADATA +1 -1
- pyopenapi_gen-0.14.2.dist-info/RECORD +132 -0
- pyopenapi_gen-0.14.0.dist-info/RECORD +0 -132
- {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.2.dist-info}/WHEEL +0 -0
- {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.2.dist-info}/entry_points.txt +0 -0
- {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.2.dist-info}/licenses/LICENSE +0 -0
@@ -5,7 +5,7 @@ Used by EndpointVisitor and related emitters.
|
|
5
5
|
|
6
6
|
import logging
|
7
7
|
import re
|
8
|
-
from typing import Any,
|
8
|
+
from typing import Any, List
|
9
9
|
|
10
10
|
from pyopenapi_gen import IROperation, IRParameter, IRRequestBody, IRResponse, IRSchema
|
11
11
|
from pyopenapi_gen.context.render_context import RenderContext
|
@@ -17,7 +17,7 @@ from ..types.services.type_service import UnifiedTypeService
|
|
17
17
|
logger = logging.getLogger(__name__)
|
18
18
|
|
19
19
|
|
20
|
-
def get_params(op: IROperation, context: RenderContext, schemas:
|
20
|
+
def get_params(op: IROperation, context: RenderContext, schemas: dict[str, IRSchema]) -> List[dict[str, Any]]:
|
21
21
|
"""
|
22
22
|
Returns a list of dicts with name, type, default, and required for template rendering.
|
23
23
|
Requires the full schema dictionary for type resolution.
|
@@ -37,7 +37,7 @@ def get_params(op: IROperation, context: RenderContext, schemas: Dict[str, IRSch
|
|
37
37
|
return params
|
38
38
|
|
39
39
|
|
40
|
-
def get_param_type(param: IRParameter, context: RenderContext, schemas:
|
40
|
+
def get_param_type(param: IRParameter, context: RenderContext, schemas: dict[str, IRSchema]) -> str:
|
41
41
|
"""Returns the Python type hint for a parameter, resolving references using the schemas dict."""
|
42
42
|
# Use unified service for type resolution
|
43
43
|
type_service = UnifiedTypeService(schemas)
|
@@ -59,7 +59,7 @@ def get_param_type(param: IRParameter, context: RenderContext, schemas: Dict[str
|
|
59
59
|
return py_type
|
60
60
|
|
61
61
|
|
62
|
-
def get_request_body_type(body: IRRequestBody, context: RenderContext, schemas:
|
62
|
+
def get_request_body_type(body: IRRequestBody, context: RenderContext, schemas: dict[str, IRSchema]) -> str:
|
63
63
|
"""Returns the Python type hint for a request body, resolving references using the schemas dict."""
|
64
64
|
# Prefer application/json schema if available
|
65
65
|
json_schema = body.content.get("application/json")
|
@@ -70,11 +70,11 @@ def get_request_body_type(body: IRRequestBody, context: RenderContext, schemas:
|
|
70
70
|
if py_type.startswith(".") and not py_type.startswith(".."):
|
71
71
|
py_type = "models" + py_type
|
72
72
|
|
73
|
-
# If the resolved type is 'Any' for a JSON body, default to
|
73
|
+
# If the resolved type is 'Any' for a JSON body, default to dict[str, Any]
|
74
74
|
if py_type == "Any":
|
75
75
|
context.add_import("typing", "Dict")
|
76
76
|
# context.add_import("typing", "Any") # Already added by the fallback or TypeHelper
|
77
|
-
return "
|
77
|
+
return "dict[str, Any]"
|
78
78
|
return py_type
|
79
79
|
# Fallback for other content types (e.g., octet-stream)
|
80
80
|
# TODO: Handle other types more specifically if needed
|
@@ -89,7 +89,7 @@ def get_request_body_type(body: IRRequestBody, context: RenderContext, schemas:
|
|
89
89
|
def get_return_type(
|
90
90
|
op: IROperation,
|
91
91
|
context: RenderContext,
|
92
|
-
schemas:
|
92
|
+
schemas: dict[str, IRSchema],
|
93
93
|
) -> tuple[str | None, bool]:
|
94
94
|
"""
|
95
95
|
DEPRECATED: Use get_return_type_unified instead.
|
@@ -136,7 +136,7 @@ def get_return_type(
|
|
136
136
|
return (py_type, False)
|
137
137
|
|
138
138
|
|
139
|
-
def _get_primary_response(op: IROperation) ->
|
139
|
+
def _get_primary_response(op: IROperation) -> IRResponse | None:
|
140
140
|
"""Helper to find the best primary success response."""
|
141
141
|
resp = None
|
142
142
|
# Prioritize 200, 201, 202, 204
|
@@ -158,7 +158,7 @@ def _get_primary_response(op: IROperation) -> Optional[IRResponse]:
|
|
158
158
|
return None
|
159
159
|
|
160
160
|
|
161
|
-
def _get_response_schema_and_content_type(resp: IRResponse) -> tuple[
|
161
|
+
def _get_response_schema_and_content_type(resp: IRResponse) -> tuple[IRSchema | None, str | None]:
|
162
162
|
"""Helper to get the schema and content type from a response."""
|
163
163
|
if not resp.content:
|
164
164
|
return None, None
|
@@ -179,7 +179,7 @@ def _get_response_schema_and_content_type(resp: IRResponse) -> tuple[Optional[IR
|
|
179
179
|
return resp.content.get(mt), mt
|
180
180
|
|
181
181
|
|
182
|
-
def _find_resource_schema(update_schema_name: str, schemas:
|
182
|
+
def _find_resource_schema(update_schema_name: str, schemas: dict[str, IRSchema]) -> IRSchema | None:
|
183
183
|
"""
|
184
184
|
Given an update schema name (e.g. 'TenantUpdate'), try to find the corresponding
|
185
185
|
resource schema (e.g. 'Tenant') in the schemas dictionary.
|
@@ -205,7 +205,7 @@ def _find_resource_schema(update_schema_name: str, schemas: Dict[str, IRSchema])
|
|
205
205
|
return None
|
206
206
|
|
207
207
|
|
208
|
-
def _infer_type_from_path(path: str, schemas:
|
208
|
+
def _infer_type_from_path(path: str, schemas: dict[str, IRSchema]) -> IRSchema | None:
|
209
209
|
"""
|
210
210
|
Infers a response type from a path. This is used when a response schema is not specified.
|
211
211
|
|
@@ -350,8 +350,8 @@ def merge_params_with_model_fields(
|
|
350
350
|
op: IROperation,
|
351
351
|
model_schema: IRSchema,
|
352
352
|
context: RenderContext,
|
353
|
-
schemas:
|
354
|
-
) -> List[
|
353
|
+
schemas: dict[str, IRSchema],
|
354
|
+
) -> List[dict[str, Any]]:
|
355
355
|
"""
|
356
356
|
Merge endpoint parameters with required model fields for function signatures.
|
357
357
|
- Ensures all required model fields are present as parameters (without duplication).
|
@@ -484,7 +484,7 @@ def get_type_for_specific_response(
|
|
484
484
|
ctx.add_import("typing", "AsyncIterator")
|
485
485
|
ctx.add_import("typing", "Dict")
|
486
486
|
ctx.add_import("typing", "Any")
|
487
|
-
return "AsyncIterator[
|
487
|
+
return "AsyncIterator[dict[str, Any]]"
|
488
488
|
|
489
489
|
return final_py_type
|
490
490
|
|
@@ -504,9 +504,7 @@ def _is_binary_stream_content(resp_ir: IRResponse) -> bool:
|
|
504
504
|
)
|
505
505
|
|
506
506
|
|
507
|
-
def _get_item_type_from_schema(
|
508
|
-
resp_ir: IRResponse, all_schemas: dict[str, IRSchema], ctx: RenderContext
|
509
|
-
) -> Optional[str]:
|
507
|
+
def _get_item_type_from_schema(resp_ir: IRResponse, all_schemas: dict[str, IRSchema], ctx: RenderContext) -> str | None:
|
510
508
|
"""Extract item type from schema for streaming responses."""
|
511
509
|
schema, _ = _get_response_schema_and_content_type(resp_ir)
|
512
510
|
if not schema:
|
@@ -528,7 +526,7 @@ def get_python_type_for_response_body(resp_ir: IRResponse, all_schemas: dict[str
|
|
528
526
|
return type_service.resolve_schema_type(schema, ctx, required=True)
|
529
527
|
|
530
528
|
|
531
|
-
def get_schema_from_response(resp_ir: IRResponse, all_schemas: dict[str, IRSchema]) ->
|
529
|
+
def get_schema_from_response(resp_ir: IRResponse, all_schemas: dict[str, IRSchema]) -> IRSchema | None:
|
532
530
|
"""Get the schema from a response object."""
|
533
531
|
schema, _ = _get_response_schema_and_content_type(resp_ir)
|
534
532
|
return schema
|
@@ -7,7 +7,7 @@ come from OpenAPI 3.1 specifications, especially nullable types.
|
|
7
7
|
|
8
8
|
import logging
|
9
9
|
import re
|
10
|
-
from typing import List
|
10
|
+
from typing import List
|
11
11
|
|
12
12
|
logger = logging.getLogger(__name__)
|
13
13
|
|
@@ -28,9 +28,9 @@ class TypeCleaner:
|
|
28
28
|
malformed type expressions.
|
29
29
|
|
30
30
|
For example:
|
31
|
-
-
|
31
|
+
- dict[str, Any, None] -> dict[str, Any]
|
32
32
|
- List[JsonValue, None] -> List[JsonValue]
|
33
|
-
-
|
33
|
+
- Any, None | None -> Any | None
|
34
34
|
|
35
35
|
Args:
|
36
36
|
type_str: The type string to clean
|
@@ -42,6 +42,14 @@ class TypeCleaner:
|
|
42
42
|
if not type_str or "[" not in type_str:
|
43
43
|
return type_str
|
44
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
|
+
|
45
53
|
# Handle edge cases
|
46
54
|
result = cls._handle_special_cases(type_str)
|
47
55
|
if result:
|
@@ -53,52 +61,52 @@ class TypeCleaner:
|
|
53
61
|
return type_str
|
54
62
|
|
55
63
|
# Handle each container type differently
|
64
|
+
result = type_str
|
56
65
|
if container == "Union":
|
57
|
-
|
66
|
+
result = cls._clean_union_type(type_str)
|
58
67
|
elif container == "List":
|
59
|
-
|
60
|
-
elif container == "Dict":
|
61
|
-
|
68
|
+
result = cls._clean_list_type(type_str)
|
69
|
+
elif container == "Dict" or container == "dict":
|
70
|
+
result = cls._clean_dict_type(type_str)
|
62
71
|
elif container == "Optional":
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
return type_str
|
72
|
+
result = cls._clean_optional_type(type_str)
|
73
|
+
|
74
|
+
return result
|
67
75
|
|
68
76
|
@classmethod
|
69
|
-
def _handle_special_cases(cls, type_str: str) ->
|
77
|
+
def _handle_special_cases(cls, type_str: str) -> str | None:
|
70
78
|
"""Handle special cases and edge conditions."""
|
71
79
|
# Special cases for empty containers
|
72
80
|
if type_str == "Union[]":
|
73
81
|
return "Any"
|
74
|
-
if type_str == "
|
75
|
-
return "
|
82
|
+
if type_str == "None | None":
|
83
|
+
return "Any | None"
|
76
84
|
|
77
85
|
# Handle incomplete syntax
|
78
|
-
if type_str == "
|
79
|
-
return "
|
86
|
+
if type_str == "dict[str,":
|
87
|
+
return "dict[str,"
|
80
88
|
|
81
89
|
# Handle specific special cases that are required by tests
|
82
90
|
special_cases = {
|
83
91
|
# OpenAPI 3.1 special case - this needs to be kept as is
|
84
|
-
"List[Union[
|
85
|
-
# The complex nested type test case
|
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
|
86
94
|
(
|
87
|
-
"Union[
|
88
|
-
"List[Union[
|
89
|
-
"Optional[
|
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]]]"
|
90
98
|
): (
|
91
|
-
"Union[
|
92
|
-
"List[Union[
|
93
|
-
"
|
99
|
+
"Union[dict[str, List[dict[str, Any]]], "
|
100
|
+
"List[Union[dict[str, Any], str, None]], "
|
101
|
+
"dict[str, Union[str, int, None]] | None]"
|
94
102
|
),
|
95
103
|
# Real-world case from EmbeddingFlat
|
96
104
|
(
|
97
|
-
"Union[
|
98
|
-
"
|
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]"
|
99
107
|
): (
|
100
|
-
"Union[
|
101
|
-
"
|
108
|
+
"Union[dict[str, Any], List[Union[dict[str, Any], List[JsonValue], "
|
109
|
+
"Any | None, bool, float, str, None]], Any | None, bool, float, str]"
|
102
110
|
),
|
103
111
|
}
|
104
112
|
|
@@ -107,19 +115,19 @@ class TypeCleaner:
|
|
107
115
|
|
108
116
|
# Special case for the real-world case in a different format
|
109
117
|
if (
|
110
|
-
"Union[
|
111
|
-
"
|
112
|
-
and "
|
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
|
113
121
|
):
|
114
122
|
return (
|
115
123
|
"Union["
|
116
|
-
"
|
124
|
+
"dict[str, Any], "
|
117
125
|
"List["
|
118
126
|
"Union["
|
119
|
-
"
|
127
|
+
"dict[str, Any], List[JsonValue], Any | None, bool, float, str, None"
|
120
128
|
"]"
|
121
129
|
"], "
|
122
|
-
"
|
130
|
+
"Any | None, "
|
123
131
|
"bool, "
|
124
132
|
"float, "
|
125
133
|
"str"
|
@@ -129,7 +137,7 @@ class TypeCleaner:
|
|
129
137
|
return None
|
130
138
|
|
131
139
|
@classmethod
|
132
|
-
def _get_container_type(cls, type_str: str) ->
|
140
|
+
def _get_container_type(cls, type_str: str) -> str | None:
|
133
141
|
"""Extract the container type from a type string."""
|
134
142
|
match = re.match(r"^([A-Za-z0-9_]+)\[", type_str)
|
135
143
|
if match:
|
@@ -142,7 +150,7 @@ class TypeCleaner:
|
|
142
150
|
# Common error pattern: Dict with extra params
|
143
151
|
dict_pattern = re.compile(r"Dict\[([^,\[\]]+),\s*([^,\[\]]+)(?:,\s*[^,\[\]]+)*(?:,\s*None)?\]")
|
144
152
|
if dict_pattern.search(type_str):
|
145
|
-
type_str = dict_pattern.sub(r"
|
153
|
+
type_str = dict_pattern.sub(r"dict[\1, \2]", type_str)
|
146
154
|
|
147
155
|
# Handle simple List with extra params
|
148
156
|
list_pattern = re.compile(r"List\[([^,\[\]]+)(?:,\s*[^,\[\]]+)*(?:,\s*None)?\]")
|
@@ -152,7 +160,7 @@ class TypeCleaner:
|
|
152
160
|
# Handle simple Optional with None
|
153
161
|
optional_pattern = re.compile(r"Optional\[([^,\[\]]+)(?:,\s*None)?\]")
|
154
162
|
if optional_pattern.search(type_str):
|
155
|
-
type_str = optional_pattern.sub(r"
|
163
|
+
type_str = optional_pattern.sub(r"\1 | None", type_str)
|
156
164
|
|
157
165
|
return type_str
|
158
166
|
|
@@ -252,9 +260,14 @@ class TypeCleaner:
|
|
252
260
|
@classmethod
|
253
261
|
def _clean_dict_type(cls, type_str: str) -> str:
|
254
262
|
"""Clean a Dict type string. Ensures Dict has exactly two parameters."""
|
255
|
-
|
256
|
-
|
257
|
-
|
263
|
+
# Handle both Dict[ and dict[ (support both uppercase and lowercase)
|
264
|
+
is_uppercase = type_str.startswith("Dict[")
|
265
|
+
prefix = "Dict[" if is_uppercase else "dict["
|
266
|
+
result_prefix = prefix.lower() # Always use lowercase in result
|
267
|
+
|
268
|
+
content = type_str[len(prefix) : -1].strip()
|
269
|
+
if not content: # Handles dict[] or Dict[]
|
270
|
+
return f"{result_prefix}Any, Any]"
|
258
271
|
|
259
272
|
params = cls._split_at_top_level_commas(content)
|
260
273
|
|
@@ -262,52 +275,52 @@ class TypeCleaner:
|
|
262
275
|
# Dict expects two parameters. Take the first two, clean them.
|
263
276
|
cleaned_key_type = cls.clean_type_parameters(params[0])
|
264
277
|
cleaned_value_type = cls.clean_type_parameters(params[1])
|
265
|
-
# Ignore further params if malformed input like
|
278
|
+
# Ignore further params if malformed input like dict[A, B, C]
|
266
279
|
if len(params) > 2:
|
267
280
|
logger.warning(f"TypeCleaner: Dict '{type_str}' had {len(params)} params. Truncating to first two.")
|
268
|
-
return f"
|
281
|
+
return f"{result_prefix}{cleaned_key_type}, {cleaned_value_type}]"
|
269
282
|
elif len(params) == 1:
|
270
283
|
# Only one parameter provided for Dict, assume it's the key, value defaults to Any.
|
271
284
|
cleaned_key_type = cls.clean_type_parameters(params[0])
|
272
285
|
logger.warning(
|
273
286
|
f"TypeCleaner: Dict '{type_str}' had only one param. "
|
274
|
-
f"Defaulting value to Any:
|
287
|
+
f"Defaulting value to Any: {result_prefix}{cleaned_key_type}, Any]"
|
275
288
|
)
|
276
|
-
return f"
|
289
|
+
return f"{result_prefix}{cleaned_key_type}, Any]"
|
277
290
|
else:
|
278
291
|
# No parameters found after split, or content was empty but not caught.
|
279
292
|
logger.warning(
|
280
293
|
f"TypeCleaner: Dict '{type_str}' content '{content}' "
|
281
|
-
f"yielded no/insufficient parameters. Defaulting to
|
294
|
+
f"yielded no/insufficient parameters. Defaulting to {result_prefix}Any, Any]."
|
282
295
|
)
|
283
|
-
return "
|
296
|
+
return f"{result_prefix}Any, Any]"
|
284
297
|
|
285
298
|
@classmethod
|
286
299
|
def _clean_optional_type(cls, type_str: str) -> str:
|
287
300
|
"""Clean an Optional type string. Ensures Optional has exactly one parameter."""
|
288
301
|
content = type_str[len("Optional[") : -1].strip()
|
289
302
|
if not content: # Handles Optional[]
|
290
|
-
return "
|
303
|
+
return "Any | None"
|
291
304
|
|
292
305
|
params = cls._split_at_top_level_commas(content)
|
293
306
|
|
294
307
|
if params:
|
295
308
|
# Optional should only have one parameter. Take the first.
|
296
309
|
cleaned_param = cls.clean_type_parameters(params[0])
|
297
|
-
if cleaned_param == "None": # Handles
|
298
|
-
return "
|
299
|
-
# Ignore further params if malformed input like
|
310
|
+
if cleaned_param == "None": # Handles None | None after cleaning inner part
|
311
|
+
return "Any | None"
|
312
|
+
# Ignore further params if malformed input like A, B | None
|
300
313
|
if len(params) > 1:
|
301
314
|
logger.warning(
|
302
315
|
f"TypeCleaner: Optional '{type_str}' had {len(params)} params. Using first: '{params[0]}'."
|
303
316
|
)
|
304
|
-
return f"
|
317
|
+
return f"{cleaned_param} | None"
|
305
318
|
else:
|
306
319
|
logger.warning(
|
307
320
|
f"TypeCleaner: Optional '{type_str}' content '{content}' "
|
308
|
-
f"yielded no parameters. Defaulting to
|
321
|
+
f"yielded no parameters. Defaulting to Any | None."
|
309
322
|
)
|
310
|
-
return "
|
323
|
+
return "Any | None"
|
311
324
|
|
312
325
|
@classmethod
|
313
326
|
def _remove_none_from_lists(cls, type_str: str) -> str:
|
@@ -1,7 +1,7 @@
|
|
1
1
|
"""Helper functions for determining Python types and managing related imports from IRSchema."""
|
2
2
|
|
3
3
|
import logging
|
4
|
-
from typing import
|
4
|
+
from typing import Set
|
5
5
|
|
6
6
|
from pyopenapi_gen import IRSchema
|
7
7
|
from pyopenapi_gen.context.render_context import RenderContext
|
@@ -21,10 +21,10 @@ class TypeHelper:
|
|
21
21
|
"""
|
22
22
|
|
23
23
|
# Cache for circular references detection
|
24
|
-
_circular_refs_cache:
|
24
|
+
_circular_refs_cache: dict[str, Set[str]] = {}
|
25
25
|
|
26
26
|
@staticmethod
|
27
|
-
def detect_circular_references(schemas:
|
27
|
+
def detect_circular_references(schemas: dict[str, IRSchema]) -> Set[str]:
|
28
28
|
"""
|
29
29
|
Detect circular references in a set of schemas.
|
30
30
|
|
@@ -40,7 +40,7 @@ class TypeHelper:
|
|
40
40
|
return TypeHelper._circular_refs_cache[cache_key]
|
41
41
|
|
42
42
|
circular_refs: Set[str] = set()
|
43
|
-
visited:
|
43
|
+
visited: dict[str, Set[str]] = {}
|
44
44
|
|
45
45
|
def visit(schema_name: str, path: Set[str]) -> None:
|
46
46
|
"""Visit a schema and check for circular references."""
|
@@ -80,13 +80,13 @@ class TypeHelper:
|
|
80
80
|
|
81
81
|
@staticmethod
|
82
82
|
def get_python_type_for_schema(
|
83
|
-
schema:
|
84
|
-
all_schemas:
|
83
|
+
schema: IRSchema | None,
|
84
|
+
all_schemas: dict[str, IRSchema],
|
85
85
|
context: RenderContext,
|
86
86
|
required: bool,
|
87
87
|
resolve_alias_target: bool = False,
|
88
88
|
render_mode: str = "field", # Literal["field", "alias_target"]
|
89
|
-
parent_schema_name:
|
89
|
+
parent_schema_name: str | None = None,
|
90
90
|
) -> str:
|
91
91
|
"""
|
92
92
|
Determines the Python type string for a given IRSchema.
|
@@ -1,7 +1,7 @@
|
|
1
1
|
"""Resolves IRSchema to Python List types."""
|
2
2
|
|
3
3
|
import logging
|
4
|
-
from typing import TYPE_CHECKING
|
4
|
+
from typing import TYPE_CHECKING
|
5
5
|
|
6
6
|
from pyopenapi_gen import IRSchema
|
7
7
|
from pyopenapi_gen.context.render_context import RenderContext
|
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
|
|
15
15
|
class ArrayTypeResolver:
|
16
16
|
"""Resolves IRSchema instances of type 'array'."""
|
17
17
|
|
18
|
-
def __init__(self, context: RenderContext, all_schemas:
|
18
|
+
def __init__(self, context: RenderContext, all_schemas: dict[str, IRSchema], main_resolver: "SchemaTypeResolver"):
|
19
19
|
self.context = context
|
20
20
|
self.all_schemas = all_schemas
|
21
21
|
self.main_resolver = main_resolver # For resolving item types
|
@@ -23,9 +23,9 @@ class ArrayTypeResolver:
|
|
23
23
|
def resolve(
|
24
24
|
self,
|
25
25
|
schema: IRSchema,
|
26
|
-
parent_name_hint:
|
26
|
+
parent_name_hint: str | None = None,
|
27
27
|
resolve_alias_target: bool = False,
|
28
|
-
) ->
|
28
|
+
) -> str | None:
|
29
29
|
"""
|
30
30
|
Resolves an IRSchema of `type: "array"` to a Python `List[...]` type string.
|
31
31
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
"""Resolves IRSchema composition types (anyOf, oneOf, allOf)."""
|
2
2
|
|
3
3
|
import logging
|
4
|
-
from typing import TYPE_CHECKING,
|
4
|
+
from typing import TYPE_CHECKING, List
|
5
5
|
|
6
6
|
from pyopenapi_gen import IRSchema
|
7
7
|
from pyopenapi_gen.context.render_context import RenderContext
|
@@ -15,19 +15,19 @@ logger = logging.getLogger(__name__)
|
|
15
15
|
class CompositionTypeResolver:
|
16
16
|
"""Resolves IRSchema instances with anyOf, oneOf, or allOf."""
|
17
17
|
|
18
|
-
def __init__(self, context: RenderContext, all_schemas:
|
18
|
+
def __init__(self, context: RenderContext, all_schemas: dict[str, IRSchema], main_resolver: "SchemaTypeResolver"):
|
19
19
|
self.context = context
|
20
20
|
self.all_schemas = all_schemas
|
21
21
|
self.main_resolver = main_resolver # For resolving member types
|
22
22
|
|
23
|
-
def resolve(self, schema: IRSchema) ->
|
23
|
+
def resolve(self, schema: IRSchema) -> str | None:
|
24
24
|
"""
|
25
25
|
Handles 'anyOf', 'oneOf', 'allOf' and returns a Python type string.
|
26
26
|
'anyOf'/'oneOf' -> Union[...]
|
27
27
|
'allOf' -> Type of first schema (simplification)
|
28
28
|
"""
|
29
|
-
composition_schemas:
|
30
|
-
composition_keyword:
|
29
|
+
composition_schemas: List[IRSchema] | None = None
|
30
|
+
composition_keyword: str | None = None
|
31
31
|
|
32
32
|
if schema.any_of is not None:
|
33
33
|
composition_schemas = schema.any_of
|
@@ -1,8 +1,6 @@
|
|
1
1
|
"""Finalizes and cleans Python type strings."""
|
2
2
|
|
3
3
|
import logging
|
4
|
-
from typing import Dict # Alias to avoid clash
|
5
|
-
from typing import Optional as TypingOptional
|
6
4
|
|
7
5
|
from pyopenapi_gen import IRSchema
|
8
6
|
from pyopenapi_gen.context.render_context import RenderContext
|
@@ -14,11 +12,11 @@ logger = logging.getLogger(__name__)
|
|
14
12
|
class TypeFinalizer:
|
15
13
|
"""Handles final wrapping (Optional) and cleaning of type strings."""
|
16
14
|
|
17
|
-
def __init__(self, context: RenderContext, all_schemas:
|
15
|
+
def __init__(self, context: RenderContext, all_schemas: dict[str, IRSchema] | None = None):
|
18
16
|
self.context = context
|
19
17
|
self.all_schemas = all_schemas if all_schemas is not None else {}
|
20
18
|
|
21
|
-
def finalize(self, py_type:
|
19
|
+
def finalize(self, py_type: str | None, schema: IRSchema, required: bool) -> str:
|
22
20
|
"""Wraps with Optional if needed, cleans the type string, and ensures typing imports."""
|
23
21
|
if py_type is None:
|
24
22
|
logger.warning(
|
@@ -28,26 +26,32 @@ class TypeFinalizer:
|
|
28
26
|
self.context.add_import("typing", "Any")
|
29
27
|
py_type = "Any"
|
30
28
|
|
31
|
-
|
32
|
-
cleaned_type = self._clean_type(
|
29
|
+
# CRITICAL: Clean BEFORE wrapping to prevent TypeCleaner from breaking "Union[X] | None" patterns
|
30
|
+
cleaned_type = self._clean_type(py_type)
|
31
|
+
optional_type = self._wrap_with_optional_if_needed(cleaned_type, schema, required)
|
33
32
|
|
34
|
-
# Ensure imports for common typing constructs that might have been introduced by cleaning
|
35
|
-
|
33
|
+
# Ensure imports for common typing constructs that might have been introduced by cleaning or wrapping
|
34
|
+
final_type = optional_type # Use the wrapped type for import analysis
|
35
|
+
if "dict[" in final_type or final_type == "Dict":
|
36
36
|
self.context.add_import("typing", "Dict")
|
37
|
-
if "List[" in
|
37
|
+
if "List[" in final_type or final_type == "List":
|
38
38
|
self.context.add_import("typing", "List")
|
39
|
-
if "Tuple[" in
|
39
|
+
if "Tuple[" in final_type or final_type == "Tuple": # Tuple might also appear bare
|
40
40
|
self.context.add_import("typing", "Tuple")
|
41
|
-
if "Union[" in
|
41
|
+
if "Union[" in final_type:
|
42
42
|
self.context.add_import("typing", "Union")
|
43
43
|
# Optional is now handled entirely by _wrap_with_optional_if_needed and not here
|
44
|
-
if
|
44
|
+
if final_type == "Any" or final_type == "Any | None": # Ensure Any is imported if it's the final type
|
45
45
|
self.context.add_import("typing", "Any")
|
46
46
|
|
47
|
-
return
|
47
|
+
return final_type
|
48
48
|
|
49
49
|
def _wrap_with_optional_if_needed(self, py_type: str, schema_being_wrapped: IRSchema, required: bool) -> str:
|
50
|
-
"""Wraps the Python type string with `
|
50
|
+
"""Wraps the Python type string with `... | None` if necessary.
|
51
|
+
|
52
|
+
Note: Modern Python 3.10+ uses X | None syntax exclusively.
|
53
|
+
Optional[X] should NEVER appear here - our unified type system generates X | None directly.
|
54
|
+
"""
|
51
55
|
is_considered_optional_by_usage = not required or schema_being_wrapped.is_nullable is True
|
52
56
|
|
53
57
|
if not is_considered_optional_by_usage:
|
@@ -56,12 +60,25 @@ class TypeFinalizer:
|
|
56
60
|
# At this point, usage implies optional. Now check if py_type inherently is.
|
57
61
|
|
58
62
|
if py_type == "Any":
|
59
|
-
|
60
|
-
return "
|
63
|
+
# Modern Python 3.10+ doesn't need Optional import for | None syntax
|
64
|
+
return "Any | None" # Any is special, always wrap if usage is optional.
|
65
|
+
|
66
|
+
# SANITY CHECK: Unified type system should never produce Optional[X]
|
67
|
+
if py_type.startswith("Optional[") and py_type.endswith("]"):
|
68
|
+
logger.error(
|
69
|
+
f"❌ ARCHITECTURE VIOLATION: Received legacy Optional[X] type: {py_type}. "
|
70
|
+
f"This should NEVER happen - unified type system generates X | None directly. "
|
71
|
+
f"Schema: {schema_being_wrapped.name or 'anonymous'}. "
|
72
|
+
f"This indicates a bug in the type resolution pipeline."
|
73
|
+
)
|
74
|
+
# Defensive conversion (but this indicates a serious bug upstream)
|
75
|
+
inner_type = py_type[9:-1] # Remove "Optional[" and "]"
|
76
|
+
logger.warning(f"⚠️ Converted to modern syntax: {inner_type} | None")
|
77
|
+
return f"{inner_type} | None"
|
61
78
|
|
62
|
-
# If already
|
63
|
-
if py_type.
|
64
|
-
return py_type # Already
|
79
|
+
# If already has | None (modern style), don't add again
|
80
|
+
if " | None" in py_type or py_type.endswith("| None"):
|
81
|
+
return py_type # Already has | None union syntax.
|
65
82
|
|
66
83
|
is_union_with_none = "Union[" in py_type and (
|
67
84
|
", None]" in py_type or "[None," in py_type or ", None," in py_type or py_type == "Union[None]"
|
@@ -80,9 +97,8 @@ class TypeFinalizer:
|
|
80
97
|
if referenced_schema.is_nullable:
|
81
98
|
return py_type
|
82
99
|
|
83
|
-
#
|
84
|
-
|
85
|
-
return f"Optional[{py_type}]"
|
100
|
+
# Wrap type with modern | None syntax (no Optional import needed in Python 3.10+)
|
101
|
+
return f"{py_type} | None"
|
86
102
|
|
87
103
|
def _clean_type(self, type_str: str) -> str:
|
88
104
|
"""Cleans a Python type string using TypeCleaner."""
|
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
import logging
|
4
4
|
import os
|
5
|
-
from typing import Dict, Optional
|
6
5
|
|
7
6
|
from pyopenapi_gen import IRSchema
|
8
7
|
from pyopenapi_gen.context.render_context import RenderContext
|
@@ -14,7 +13,7 @@ logger = logging.getLogger(__name__)
|
|
14
13
|
class NamedTypeResolver:
|
15
14
|
"""Resolves IRSchema instances that refer to named models/enums."""
|
16
15
|
|
17
|
-
def __init__(self, context: RenderContext, all_schemas:
|
16
|
+
def __init__(self, context: RenderContext, all_schemas: dict[str, IRSchema]):
|
18
17
|
self.context = context
|
19
18
|
self.all_schemas = all_schemas
|
20
19
|
|
@@ -59,7 +58,7 @@ class NamedTypeResolver:
|
|
59
58
|
parts = module_name.split("_")
|
60
59
|
return "".join(word.capitalize() for word in parts)
|
61
60
|
|
62
|
-
def resolve(self, schema: IRSchema, resolve_alias_target: bool = False) ->
|
61
|
+
def resolve(self, schema: IRSchema, resolve_alias_target: bool = False) -> str | None:
|
63
62
|
"""
|
64
63
|
Resolves an IRSchema that refers to a named model/enum, or an inline named enum.
|
65
64
|
|
@@ -69,7 +68,7 @@ class NamedTypeResolver:
|
|
69
68
|
*target* of an alias. If false, it should return the alias name itself.
|
70
69
|
|
71
70
|
Returns:
|
72
|
-
A Python type string for the resolved schema, e.g., "MyModel", "
|
71
|
+
A Python type string for the resolved schema, e.g., "MyModel", "MyModel | None".
|
73
72
|
"""
|
74
73
|
|
75
74
|
if schema.name and schema.name in self.all_schemas:
|
@@ -147,7 +146,7 @@ class NamedTypeResolver:
|
|
147
146
|
|
148
147
|
elif schema.enum:
|
149
148
|
# This is an INLINE enum definition (not a reference to a global enum)
|
150
|
-
enum_name:
|
149
|
+
enum_name: str | None = None
|
151
150
|
if schema.name: # If the inline enum has a name, it will be generated as a named enum class
|
152
151
|
enum_name = NameSanitizer.sanitize_class_name(schema.name)
|
153
152
|
module_name = NameSanitizer.sanitize_module_name(schema.name)
|