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.
Files changed (80) hide show
  1. pyopenapi_gen/cli.py +3 -3
  2. pyopenapi_gen/context/import_collector.py +10 -10
  3. pyopenapi_gen/context/render_context.py +13 -13
  4. pyopenapi_gen/core/auth/plugins.py +7 -7
  5. pyopenapi_gen/core/http_status_codes.py +2 -4
  6. pyopenapi_gen/core/http_transport.py +19 -19
  7. pyopenapi_gen/core/loader/operations/parser.py +2 -2
  8. pyopenapi_gen/core/loader/operations/request_body.py +3 -3
  9. pyopenapi_gen/core/loader/parameters/parser.py +3 -3
  10. pyopenapi_gen/core/loader/responses/parser.py +2 -2
  11. pyopenapi_gen/core/loader/schemas/extractor.py +4 -4
  12. pyopenapi_gen/core/pagination.py +3 -3
  13. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +3 -3
  14. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +2 -2
  15. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +3 -3
  16. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +3 -3
  17. pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +2 -2
  18. pyopenapi_gen/core/parsing/common/type_parser.py +2 -3
  19. pyopenapi_gen/core/parsing/context.py +10 -10
  20. pyopenapi_gen/core/parsing/cycle_helpers.py +5 -2
  21. pyopenapi_gen/core/parsing/keywords/all_of_parser.py +5 -5
  22. pyopenapi_gen/core/parsing/keywords/any_of_parser.py +4 -4
  23. pyopenapi_gen/core/parsing/keywords/array_items_parser.py +4 -4
  24. pyopenapi_gen/core/parsing/keywords/one_of_parser.py +4 -4
  25. pyopenapi_gen/core/parsing/keywords/properties_parser.py +5 -5
  26. pyopenapi_gen/core/parsing/schema_finalizer.py +15 -15
  27. pyopenapi_gen/core/parsing/schema_parser.py +44 -25
  28. pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +4 -4
  29. pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +7 -4
  30. pyopenapi_gen/core/parsing/unified_cycle_detection.py +10 -10
  31. pyopenapi_gen/core/schemas.py +10 -10
  32. pyopenapi_gen/core/streaming_helpers.py +5 -7
  33. pyopenapi_gen/core/telemetry.py +4 -4
  34. pyopenapi_gen/core/utils.py +7 -7
  35. pyopenapi_gen/core/writers/code_writer.py +2 -2
  36. pyopenapi_gen/core/writers/documentation_writer.py +18 -18
  37. pyopenapi_gen/core/writers/line_writer.py +3 -3
  38. pyopenapi_gen/core/writers/python_construct_renderer.py +10 -10
  39. pyopenapi_gen/emit/models_emitter.py +2 -2
  40. pyopenapi_gen/emitters/core_emitter.py +3 -5
  41. pyopenapi_gen/emitters/endpoints_emitter.py +24 -16
  42. pyopenapi_gen/emitters/exceptions_emitter.py +4 -3
  43. pyopenapi_gen/emitters/models_emitter.py +6 -6
  44. pyopenapi_gen/generator/client_generator.py +6 -6
  45. pyopenapi_gen/helpers/endpoint_utils.py +16 -18
  46. pyopenapi_gen/helpers/type_cleaner.py +66 -53
  47. pyopenapi_gen/helpers/type_helper.py +7 -7
  48. pyopenapi_gen/helpers/type_resolution/array_resolver.py +4 -4
  49. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +5 -5
  50. pyopenapi_gen/helpers/type_resolution/finalizer.py +38 -22
  51. pyopenapi_gen/helpers/type_resolution/named_resolver.py +4 -5
  52. pyopenapi_gen/helpers/type_resolution/object_resolver.py +11 -11
  53. pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +1 -2
  54. pyopenapi_gen/helpers/type_resolution/resolver.py +2 -3
  55. pyopenapi_gen/ir.py +32 -34
  56. pyopenapi_gen/types/contracts/protocols.py +5 -5
  57. pyopenapi_gen/types/contracts/types.py +2 -3
  58. pyopenapi_gen/types/resolvers/reference_resolver.py +4 -4
  59. pyopenapi_gen/types/resolvers/response_resolver.py +6 -4
  60. pyopenapi_gen/types/resolvers/schema_resolver.py +32 -16
  61. pyopenapi_gen/types/services/type_service.py +55 -9
  62. pyopenapi_gen/types/strategies/response_strategy.py +6 -7
  63. pyopenapi_gen/visit/client_visitor.py +5 -7
  64. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +7 -7
  65. pyopenapi_gen/visit/endpoint/generators/request_generator.py +5 -5
  66. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +38 -17
  67. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +4 -4
  68. pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +17 -17
  69. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +8 -8
  70. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +13 -13
  71. pyopenapi_gen/visit/model/alias_generator.py +1 -4
  72. pyopenapi_gen/visit/model/dataclass_generator.py +139 -10
  73. pyopenapi_gen/visit/model/model_visitor.py +2 -3
  74. pyopenapi_gen/visit/visitor.py +3 -3
  75. {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.2.dist-info}/METADATA +1 -1
  76. pyopenapi_gen-0.14.2.dist-info/RECORD +132 -0
  77. pyopenapi_gen-0.14.0.dist-info/RECORD +0 -132
  78. {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.2.dist-info}/WHEEL +0 -0
  79. {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.2.dist-info}/entry_points.txt +0 -0
  80. {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, Dict, List, Optional
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: Dict[str, IRSchema]) -> List[Dict[str, Any]]:
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: Dict[str, IRSchema]) -> str:
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: Dict[str, IRSchema]) -> str:
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 Dict[str, Any]
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 "Dict[str, Any]"
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: Dict[str, IRSchema],
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) -> Optional[IRResponse]:
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[Optional[IRSchema], Optional[str]]:
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: Dict[str, IRSchema]) -> Optional[IRSchema]:
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: Dict[str, IRSchema]) -> Optional[IRSchema]:
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: Dict[str, IRSchema],
354
- ) -> List[Dict[str, Any]]:
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[Dict[str, Any]]"
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]) -> Optional[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, Optional
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
- - Dict[str, Any, None] -> Dict[str, Any]
31
+ - dict[str, Any, None] -> dict[str, Any]
32
32
  - List[JsonValue, None] -> List[JsonValue]
33
- - Optional[Any, None] -> Optional[Any]
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
- return cls._clean_union_type(type_str)
66
+ result = cls._clean_union_type(type_str)
58
67
  elif container == "List":
59
- return cls._clean_list_type(type_str)
60
- elif container == "Dict":
61
- return cls._clean_dict_type(type_str)
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
- return cls._clean_optional_type(type_str)
64
- else:
65
- # For unrecognized containers, return as is
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) -> Optional[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 == "Optional[None]":
75
- return "Optional[Any]"
82
+ if type_str == "None | None":
83
+ return "Any | None"
76
84
 
77
85
  # Handle incomplete syntax
78
- if type_str == "Dict[str,":
79
- return "Dict[str,"
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[Dict[str, Any], None]]": "List[Union[Dict[str, Any], None]]",
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[Dict[str, List[Dict[str, Any, None], None]], "
88
- "List[Union[Dict[str, Any, None], str, None]], "
89
- "Optional[Dict[str, Union[str, int, None], None]]]"
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[Dict[str, List[Dict[str, Any]]], "
92
- "List[Union[Dict[str, Any], str, None]], "
93
- "Optional[Dict[str, Union[str, int, None]]]]"
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[Dict[str, Any], List[Union[Dict[str, Any], List[JsonValue], "
98
- "Optional[Any], bool, float, str, None], None], Optional[Any], bool, float, str]"
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[Dict[str, Any], List[Union[Dict[str, Any], List[JsonValue], "
101
- "Optional[Any], bool, float, str, None]], Optional[Any], bool, float, str]"
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[Dict[str, Any], List[Union[Dict[str, Any], List[JsonValue], "
111
- "Optional[Any], bool, float, str, None], None]" in type_str
112
- and "Optional[Any], bool, float, str]" in type_str
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
- "Dict[str, Any], "
124
+ "dict[str, Any], "
117
125
  "List["
118
126
  "Union["
119
- "Dict[str, Any], List[JsonValue], Optional[Any], bool, float, str, None"
127
+ "dict[str, Any], List[JsonValue], Any | None, bool, float, str, None"
120
128
  "]"
121
129
  "], "
122
- "Optional[Any], "
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) -> Optional[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"Dict[\1, \2]", type_str)
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"Optional[\1]", type_str)
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
- content = type_str[len("Dict[") : -1].strip()
256
- if not content: # Handles Dict[]
257
- return "Dict[Any, Any]"
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 Dict[A, B, C]
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"Dict[{cleaned_key_type}, {cleaned_value_type}]"
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: Dict[{cleaned_key_type}, Any]"
287
+ f"Defaulting value to Any: {result_prefix}{cleaned_key_type}, Any]"
275
288
  )
276
- return f"Dict[{cleaned_key_type}, Any]"
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 Dict[Any, Any]."
294
+ f"yielded no/insufficient parameters. Defaulting to {result_prefix}Any, Any]."
282
295
  )
283
- return "Dict[Any, Any]"
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 "Optional[Any]"
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 Optional[None] after cleaning inner part
298
- return "Optional[Any]"
299
- # Ignore further params if malformed input like Optional[A, B]
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"Optional[{cleaned_param}]"
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 Optional[Any]."
321
+ f"yielded no parameters. Defaulting to Any | None."
309
322
  )
310
- return "Optional[Any]"
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 Dict, Optional, Set
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: Dict[str, Set[str]] = {}
24
+ _circular_refs_cache: dict[str, Set[str]] = {}
25
25
 
26
26
  @staticmethod
27
- def detect_circular_references(schemas: Dict[str, IRSchema]) -> Set[str]:
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: Dict[str, Set[str]] = {}
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: Optional[IRSchema],
84
- all_schemas: Dict[str, IRSchema],
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: Optional[str] = None,
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, Dict, Optional
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: Dict[str, IRSchema], main_resolver: "SchemaTypeResolver"):
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: Optional[str] = None,
26
+ parent_name_hint: str | None = None,
27
27
  resolve_alias_target: bool = False,
28
- ) -> Optional[str]:
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, Dict, List, Optional
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: Dict[str, IRSchema], main_resolver: "SchemaTypeResolver"):
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) -> Optional[str]:
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: Optional[List[IRSchema]] = None
30
- composition_keyword: Optional[str] = None
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: TypingOptional[Dict[str, IRSchema]] = None):
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: TypingOptional[str], schema: IRSchema, required: bool) -> str:
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
- optional_type = self._wrap_with_optional_if_needed(py_type, schema, required)
32
- cleaned_type = self._clean_type(optional_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
- if "Dict[" in cleaned_type or cleaned_type == "Dict":
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 cleaned_type or cleaned_type == "List":
37
+ if "List[" in final_type or final_type == "List":
38
38
  self.context.add_import("typing", "List")
39
- if "Tuple[" in cleaned_type or cleaned_type == "Tuple": # Tuple might also appear bare
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 cleaned_type:
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 cleaned_type == "Any": # Ensure Any is imported if it's the final type
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 cleaned_type
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 `Optional[...]` if necessary."""
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
- self.context.add_import("typing", "Optional")
60
- return "Optional[Any]" # Any is special, always wrap if usage is optional.
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 Optional, don't add import again
63
- if py_type.startswith("Optional["):
64
- return py_type # Already explicitly Optional.
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
- # Only now add the Optional import and wrap the type
84
- self.context.add_import("typing", "Optional")
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: Dict[str, IRSchema]):
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) -> Optional[str]:
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", "Optional[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: Optional[str] = None
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)