pyopenapi-gen 0.14.0__py3-none-any.whl → 0.14.1__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 +12 -12
- 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.1.dist-info}/METADATA +1 -1
- pyopenapi_gen-0.14.1.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.1.dist-info}/WHEEL +0 -0
- {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/entry_points.txt +0 -0
- {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/licenses/LICENSE +0 -0
@@ -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)
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
import logging
|
4
4
|
import os
|
5
|
-
from typing import TYPE_CHECKING
|
5
|
+
from typing import TYPE_CHECKING
|
6
6
|
|
7
7
|
from pyopenapi_gen import IRSchema
|
8
8
|
from pyopenapi_gen.context.render_context import RenderContext
|
@@ -17,7 +17,7 @@ logger = logging.getLogger(__name__)
|
|
17
17
|
class ObjectTypeResolver:
|
18
18
|
"""Resolves IRSchema instances of type 'object'."""
|
19
19
|
|
20
|
-
def __init__(self, context: RenderContext, all_schemas:
|
20
|
+
def __init__(self, context: RenderContext, all_schemas: dict[str, IRSchema], main_resolver: "SchemaTypeResolver"):
|
21
21
|
self.context = context
|
22
22
|
self.all_schemas = all_schemas
|
23
23
|
self.main_resolver = main_resolver # For resolving nested types
|
@@ -25,8 +25,8 @@ class ObjectTypeResolver:
|
|
25
25
|
def _promote_anonymous_object_schema_if_needed(
|
26
26
|
self,
|
27
27
|
schema_to_promote: IRSchema,
|
28
|
-
proposed_name_base:
|
29
|
-
) ->
|
28
|
+
proposed_name_base: str | None,
|
29
|
+
) -> str | None:
|
30
30
|
"""Gives a name to an anonymous object schema and registers it."""
|
31
31
|
if not proposed_name_base:
|
32
32
|
return None
|
@@ -58,7 +58,7 @@ class ObjectTypeResolver:
|
|
58
58
|
self.context.add_import(module_to_import_from, final_new_name)
|
59
59
|
return final_new_name
|
60
60
|
|
61
|
-
def resolve(self, schema: IRSchema, parent_schema_name_for_anon_promotion:
|
61
|
+
def resolve(self, schema: IRSchema, parent_schema_name_for_anon_promotion: str | None = None) -> str | None:
|
62
62
|
"""
|
63
63
|
Resolves an IRSchema of `type: "object"`.
|
64
64
|
Args:
|
@@ -72,7 +72,7 @@ class ObjectTypeResolver:
|
|
72
72
|
if isinstance(schema.additional_properties, bool) and schema.additional_properties:
|
73
73
|
self.context.add_import("typing", "Dict")
|
74
74
|
self.context.add_import("typing", "Any")
|
75
|
-
return "
|
75
|
+
return "dict[str, Any]"
|
76
76
|
|
77
77
|
# Path B: additionalProperties is an IRSchema instance
|
78
78
|
if isinstance(schema.additional_properties, IRSchema):
|
@@ -90,7 +90,7 @@ class ObjectTypeResolver:
|
|
90
90
|
if is_ap_schema_defined:
|
91
91
|
additional_prop_type = self.main_resolver.resolve(ap_schema_instance, required=True)
|
92
92
|
self.context.add_import("typing", "Dict")
|
93
|
-
return f"
|
93
|
+
return f"dict[str, {additional_prop_type}]"
|
94
94
|
|
95
95
|
# Path C: additionalProperties is False, None, or empty IRSchema.
|
96
96
|
if schema.properties: # Object has its own properties
|
@@ -104,11 +104,11 @@ class ObjectTypeResolver:
|
|
104
104
|
# Fallback for unpromoted anonymous object with properties
|
105
105
|
logger.warning(
|
106
106
|
f"[ObjectTypeResolver] Anonymous object with properties not promoted. "
|
107
|
-
f"->
|
107
|
+
f"-> dict[str, Any]. Schema: {schema}"
|
108
108
|
)
|
109
109
|
self.context.add_import("typing", "Dict")
|
110
110
|
self.context.add_import("typing", "Any")
|
111
|
-
return "
|
111
|
+
return "dict[str, Any]"
|
112
112
|
else: # Named object with properties
|
113
113
|
# If this named object is a component schema, ensure it's imported.
|
114
114
|
if schema.name and schema.name in self.all_schemas:
|
@@ -204,12 +204,12 @@ class ObjectTypeResolver:
|
|
204
204
|
elif schema.name: # Named object, no properties, but NOT a known component
|
205
205
|
self.context.add_import("typing", "Dict")
|
206
206
|
self.context.add_import("typing", "Any")
|
207
|
-
return "
|
207
|
+
return "dict[str, Any]"
|
208
208
|
else: # Anonymous object, no properties
|
209
209
|
if schema.additional_properties is None: # Default OpenAPI behavior allows additional props
|
210
210
|
self.context.add_import("typing", "Dict")
|
211
211
|
self.context.add_import("typing", "Any")
|
212
|
-
return "
|
212
|
+
return "dict[str, Any]"
|
213
213
|
else: # additionalProperties was False or restrictive empty schema
|
214
214
|
self.context.add_import("typing", "Any")
|
215
215
|
return "Any"
|
@@ -1,7 +1,6 @@
|
|
1
1
|
"""Resolves IRSchema to Python primitive types."""
|
2
2
|
|
3
3
|
import logging
|
4
|
-
from typing import Optional
|
5
4
|
|
6
5
|
from pyopenapi_gen import IRSchema
|
7
6
|
from pyopenapi_gen.context.render_context import RenderContext
|
@@ -15,7 +14,7 @@ class PrimitiveTypeResolver:
|
|
15
14
|
def __init__(self, context: RenderContext):
|
16
15
|
self.context = context
|
17
16
|
|
18
|
-
def resolve(self, schema: IRSchema) ->
|
17
|
+
def resolve(self, schema: IRSchema) -> str | None:
|
19
18
|
"""
|
20
19
|
Resolves an IRSchema to a Python primitive type string based on its 'type' and 'format'.
|
21
20
|
|
@@ -1,7 +1,6 @@
|
|
1
1
|
"""Orchestrates IRSchema to Python type resolution."""
|
2
2
|
|
3
3
|
import logging
|
4
|
-
from typing import Dict, Optional
|
5
4
|
|
6
5
|
from pyopenapi_gen import IRSchema
|
7
6
|
from pyopenapi_gen.context.render_context import RenderContext
|
@@ -20,7 +19,7 @@ logger = logging.getLogger(__name__)
|
|
20
19
|
class SchemaTypeResolver:
|
21
20
|
"""Orchestrates the resolution of IRSchema to Python type strings."""
|
22
21
|
|
23
|
-
def __init__(self, context: RenderContext, all_schemas:
|
22
|
+
def __init__(self, context: RenderContext, all_schemas: dict[str, IRSchema]):
|
24
23
|
self.context = context
|
25
24
|
self.all_schemas = all_schemas
|
26
25
|
|
@@ -37,7 +36,7 @@ class SchemaTypeResolver:
|
|
37
36
|
schema: IRSchema,
|
38
37
|
required: bool = True,
|
39
38
|
resolve_alias_target: bool = False,
|
40
|
-
current_schema_context_name:
|
39
|
+
current_schema_context_name: str | None = None,
|
41
40
|
) -> str:
|
42
41
|
"""
|
43
42
|
Determines the Python type string for a given IRSchema.
|