pyopenapi-gen 0.13.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.
Files changed (82) 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 +218 -0
  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/postprocess_manager.py +85 -12
  32. pyopenapi_gen/core/schemas.py +10 -10
  33. pyopenapi_gen/core/streaming_helpers.py +5 -7
  34. pyopenapi_gen/core/telemetry.py +4 -4
  35. pyopenapi_gen/core/utils.py +7 -7
  36. pyopenapi_gen/core/writers/code_writer.py +2 -2
  37. pyopenapi_gen/core/writers/documentation_writer.py +18 -18
  38. pyopenapi_gen/core/writers/line_writer.py +3 -3
  39. pyopenapi_gen/core/writers/python_construct_renderer.py +15 -11
  40. pyopenapi_gen/emit/models_emitter.py +2 -2
  41. pyopenapi_gen/emitters/core_emitter.py +3 -5
  42. pyopenapi_gen/emitters/endpoints_emitter.py +12 -12
  43. pyopenapi_gen/emitters/exceptions_emitter.py +153 -18
  44. pyopenapi_gen/emitters/models_emitter.py +6 -6
  45. pyopenapi_gen/generator/client_generator.py +10 -8
  46. pyopenapi_gen/helpers/endpoint_utils.py +16 -18
  47. pyopenapi_gen/helpers/type_cleaner.py +66 -53
  48. pyopenapi_gen/helpers/type_helper.py +7 -7
  49. pyopenapi_gen/helpers/type_resolution/array_resolver.py +4 -4
  50. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +5 -5
  51. pyopenapi_gen/helpers/type_resolution/finalizer.py +38 -22
  52. pyopenapi_gen/helpers/type_resolution/named_resolver.py +4 -5
  53. pyopenapi_gen/helpers/type_resolution/object_resolver.py +11 -11
  54. pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +1 -2
  55. pyopenapi_gen/helpers/type_resolution/resolver.py +2 -3
  56. pyopenapi_gen/ir.py +32 -34
  57. pyopenapi_gen/types/contracts/protocols.py +5 -5
  58. pyopenapi_gen/types/contracts/types.py +2 -3
  59. pyopenapi_gen/types/resolvers/reference_resolver.py +4 -4
  60. pyopenapi_gen/types/resolvers/response_resolver.py +6 -4
  61. pyopenapi_gen/types/resolvers/schema_resolver.py +32 -16
  62. pyopenapi_gen/types/services/type_service.py +55 -9
  63. pyopenapi_gen/types/strategies/response_strategy.py +6 -7
  64. pyopenapi_gen/visit/client_visitor.py +5 -7
  65. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +7 -7
  66. pyopenapi_gen/visit/endpoint/generators/request_generator.py +5 -5
  67. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +41 -19
  68. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +4 -4
  69. pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +17 -17
  70. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +8 -8
  71. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +13 -13
  72. pyopenapi_gen/visit/exception_visitor.py +54 -16
  73. pyopenapi_gen/visit/model/alias_generator.py +1 -4
  74. pyopenapi_gen/visit/model/dataclass_generator.py +139 -10
  75. pyopenapi_gen/visit/model/model_visitor.py +2 -3
  76. pyopenapi_gen/visit/visitor.py +3 -3
  77. {pyopenapi_gen-0.13.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/METADATA +1 -1
  78. pyopenapi_gen-0.14.1.dist-info/RECORD +132 -0
  79. pyopenapi_gen-0.13.0.dist-info/RECORD +0 -131
  80. {pyopenapi_gen-0.13.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/WHEEL +0 -0
  81. {pyopenapi_gen-0.13.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/entry_points.txt +0 -0
  82. {pyopenapi_gen-0.13.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, 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)
@@ -2,7 +2,7 @@
2
2
 
3
3
  import logging
4
4
  import os
5
- from typing import TYPE_CHECKING, Dict, Optional
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: Dict[str, IRSchema], main_resolver: "SchemaTypeResolver"):
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: Optional[str],
29
- ) -> Optional[str]:
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: Optional[str] = None) -> Optional[str]:
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 "Dict[str, Any]"
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"Dict[str, {additional_prop_type}]"
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"-> Dict[str, Any]. Schema: {schema}"
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 "Dict[str, Any]"
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 "Dict[str, Any]"
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 "Dict[str, Any]"
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) -> Optional[str]:
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: Dict[str, IRSchema]):
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: Optional[str] = None,
39
+ current_schema_context_name: str | None = None,
41
40
  ) -> str:
42
41
  """
43
42
  Determines the Python type string for a given IRSchema.