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
pyopenapi_gen/ir.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass, field
4
- from typing import Any, Dict, List, Optional, Union
4
+ from typing import Any, List, Union
5
5
 
6
6
  # Import NameSanitizer at the top for type hints and __post_init__ usage
7
7
  from pyopenapi_gen.core.utils import NameSanitizer
@@ -16,33 +16,33 @@ from .http_types import HTTPMethod
16
16
 
17
17
  @dataclass
18
18
  class IRSchema:
19
- name: Optional[str] = None
20
- type: Optional[str] = None # E.g., "object", "array", "string", or a reference to another schema name
21
- format: Optional[str] = None
22
- description: Optional[str] = None
19
+ name: str | None = None
20
+ type: str | None = None # E.g., "object", "array", "string", or a reference to another schema name
21
+ format: str | None = None
22
+ description: str | None = None
23
23
  required: List[str] = field(default_factory=list)
24
- properties: Dict[str, IRSchema] = field(default_factory=dict)
25
- items: Optional[IRSchema] = None # For type: "array"
26
- enum: Optional[List[Any]] = None
27
- default: Optional[Any] = None # Added default value
28
- example: Optional[Any] = None # Added example value
29
- additional_properties: Optional[Union[bool, IRSchema]] = None # True, False, or an IRSchema
24
+ properties: dict[str, IRSchema] = field(default_factory=dict)
25
+ items: IRSchema | None = None # For type: "array"
26
+ enum: List[Any] | None = None
27
+ default: Any | None = None # Added default value
28
+ example: Any | None = None # Added example value
29
+ additional_properties: Union[bool, IRSchema] | None = None # True, False, or an IRSchema
30
30
  is_nullable: bool = False
31
- any_of: Optional[List[IRSchema]] = None
32
- one_of: Optional[List[IRSchema]] = None
33
- all_of: Optional[List[IRSchema]] = None # Store the list of IRSchema objects from allOf
34
- title: Optional[str] = None # Added title
31
+ any_of: List[IRSchema] | None = None
32
+ one_of: List[IRSchema] | None = None
33
+ all_of: List[IRSchema] | None = None # Store the list of IRSchema objects from allOf
34
+ title: str | None = None # Added title
35
35
  is_data_wrapper: bool = False # True if schema is a simple {{ "data": OtherSchema }} wrapper
36
36
 
37
37
  # Internal generator flags/helpers
38
38
  _from_unresolved_ref: bool = field(
39
39
  default=False, repr=False
40
40
  ) # If this IRSchema is a placeholder for an unresolvable $ref
41
- _refers_to_schema: Optional[IRSchema] = (
41
+ _refers_to_schema: IRSchema | None = (
42
42
  None # If this schema is a reference (e.g. a promoted property), this can link to the actual definition
43
43
  )
44
44
  _is_circular_ref: bool = field(default=False, repr=False) # If this IRSchema is part of a circular reference chain
45
- _circular_ref_path: Optional[str] = field(default=None, repr=False) # Path of the circular reference
45
+ _circular_ref_path: str | None = field(default=None, repr=False) # Path of the circular reference
46
46
  _max_depth_exceeded_marker: bool = field(
47
47
  default=False, repr=False
48
48
  ) # If parsing this schema or its components exceeded max depth
@@ -50,13 +50,11 @@ class IRSchema:
50
50
  _is_name_derived: bool = field(
51
51
  default=False, repr=False
52
52
  ) # True if the name was derived (e.g. for promoted inline objects)
53
- _inline_name_resolution_path: Optional[str] = field(
54
- default=None, repr=False
55
- ) # Path used for resolving inline names
53
+ _inline_name_resolution_path: str | None = field(default=None, repr=False) # Path used for resolving inline names
56
54
 
57
55
  # Fields for storing final, de-collided names for code generation
58
- generation_name: Optional[str] = field(default=None, repr=True) # Final class/enum name
59
- final_module_stem: Optional[str] = field(default=None, repr=True) # Final module filename stem
56
+ generation_name: str | None = field(default=None, repr=True) # Final class/enum name
57
+ final_module_stem: str | None = field(default=None, repr=True) # Final module filename stem
60
58
 
61
59
  def __post_init__(self) -> None:
62
60
  # Ensure name is always a valid Python identifier if set
@@ -119,25 +117,25 @@ class IRParameter:
119
117
  param_in: str # Renamed from 'in' to avoid keyword clash, was in_: str in original __init__.py
120
118
  required: bool
121
119
  schema: IRSchema
122
- description: Optional[str] = None
123
- # example: Optional[Any] = None # This was in my latest ir.py but not __init__.py, keeping it from my version
120
+ description: str | None = None
121
+ # example: Any | None = None # This was in my latest ir.py but not __init__.py, keeping it from my version
124
122
 
125
123
 
126
124
  # Adding other IR classes from the original __init__.py structure
127
125
  @dataclass(slots=True)
128
126
  class IRResponse:
129
127
  status_code: str # can be "default" or specific status like "200"
130
- description: Optional[str]
131
- content: Dict[str, IRSchema] # media‑type → schema mapping
128
+ description: str | None
129
+ content: dict[str, IRSchema] # media‑type → schema mapping
132
130
  stream: bool = False # Indicates a binary or streaming response
133
- stream_format: Optional[str] = None # Indicates the stream type
131
+ stream_format: str | None = None # Indicates the stream type
134
132
 
135
133
 
136
134
  @dataclass(slots=True)
137
135
  class IRRequestBody:
138
136
  required: bool
139
- content: Dict[str, IRSchema] # media‑type → schema mapping
140
- description: Optional[str] = None
137
+ content: dict[str, IRSchema] # media‑type → schema mapping
138
+ description: str | None = None
141
139
 
142
140
 
143
141
  @dataclass(slots=True)
@@ -145,10 +143,10 @@ class IROperation:
145
143
  operation_id: str
146
144
  method: HTTPMethod # Enforced via enum for consistency
147
145
  path: str # e.g. "/pets/{petId}"
148
- summary: Optional[str]
149
- description: Optional[str]
146
+ summary: str | None
147
+ description: str | None
150
148
  parameters: List[IRParameter] = field(default_factory=list)
151
- request_body: Optional[IRRequestBody] = None
149
+ request_body: IRRequestBody | None = None
152
150
  responses: List[IRResponse] = field(default_factory=list)
153
151
  tags: List[str] = field(default_factory=list)
154
152
 
@@ -157,8 +155,8 @@ class IROperation:
157
155
  class IRSpec:
158
156
  title: str
159
157
  version: str
160
- description: Optional[str] = None
161
- schemas: Dict[str, IRSchema] = field(default_factory=dict)
158
+ description: str | None = None
159
+ schemas: dict[str, IRSchema] = field(default_factory=dict)
162
160
  operations: List[IROperation] = field(default_factory=list)
163
161
  servers: List[str] = field(default_factory=list)
164
162
 
@@ -1,7 +1,7 @@
1
1
  """Protocols for type resolution components."""
2
2
 
3
3
  from abc import ABC, abstractmethod
4
- from typing import Dict, Optional, Protocol, runtime_checkable
4
+ from typing import Protocol, runtime_checkable
5
5
 
6
6
  from pyopenapi_gen import IROperation, IRResponse, IRSchema
7
7
 
@@ -25,11 +25,11 @@ class ReferenceResolver(ABC):
25
25
  """Resolves OpenAPI $ref references to target schemas."""
26
26
 
27
27
  # Concrete implementations should have these attributes
28
- schemas: Dict[str, IRSchema]
29
- responses: Dict[str, IRResponse]
28
+ schemas: dict[str, IRSchema]
29
+ responses: dict[str, IRResponse]
30
30
 
31
31
  @abstractmethod
32
- def resolve_ref(self, ref: str) -> Optional[IRSchema]:
32
+ def resolve_ref(self, ref: str) -> IRSchema | None:
33
33
  """
34
34
  Resolve a $ref string to the target schema.
35
35
 
@@ -42,7 +42,7 @@ class ReferenceResolver(ABC):
42
42
  pass
43
43
 
44
44
  @abstractmethod
45
- def resolve_response_ref(self, ref: str) -> Optional[IRResponse]:
45
+ def resolve_response_ref(self, ref: str) -> IRResponse | None:
46
46
  """
47
47
  Resolve a response $ref to the target response.
48
48
 
@@ -1,7 +1,6 @@
1
1
  """Core types for type resolution."""
2
2
 
3
3
  from dataclasses import dataclass
4
- from typing import Optional
5
4
 
6
5
 
7
6
  class TypeResolutionError(Exception):
@@ -16,8 +15,8 @@ class ResolvedType:
16
15
 
17
16
  python_type: str
18
17
  needs_import: bool = False
19
- import_module: Optional[str] = None
20
- import_name: Optional[str] = None
18
+ import_module: str | None = None
19
+ import_name: str | None = None
21
20
  is_optional: bool = False
22
21
  is_forward_ref: bool = False
23
22
 
@@ -1,7 +1,7 @@
1
1
  """Reference resolver implementation."""
2
2
 
3
3
  import logging
4
- from typing import Dict, Optional
4
+ from typing import Dict
5
5
 
6
6
  from pyopenapi_gen import IRResponse, IRSchema
7
7
 
@@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
13
13
  class OpenAPIReferenceResolver(ReferenceResolver):
14
14
  """Resolves OpenAPI $ref references."""
15
15
 
16
- def __init__(self, schemas: Dict[str, IRSchema], responses: Optional[Dict[str, IRResponse]] = None):
16
+ def __init__(self, schemas: Dict[str, IRSchema], responses: Dict[str, IRResponse] | None = None):
17
17
  """
18
18
  Initialize reference resolver.
19
19
 
@@ -24,7 +24,7 @@ class OpenAPIReferenceResolver(ReferenceResolver):
24
24
  self.schemas = schemas
25
25
  self.responses = responses or {}
26
26
 
27
- def resolve_ref(self, ref: str) -> Optional[IRSchema]:
27
+ def resolve_ref(self, ref: str) -> IRSchema | None:
28
28
  """
29
29
  Resolve a schema $ref to the target schema.
30
30
 
@@ -47,7 +47,7 @@ class OpenAPIReferenceResolver(ReferenceResolver):
47
47
 
48
48
  return schema
49
49
 
50
- def resolve_response_ref(self, ref: str) -> Optional[IRResponse]:
50
+ def resolve_response_ref(self, ref: str) -> IRResponse | None:
51
51
  """
52
52
  Resolve a response $ref to the target response.
53
53
 
@@ -1,7 +1,10 @@
1
1
  """Response type resolver implementation."""
2
2
 
3
3
  import logging
4
- from typing import Optional
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ pass
5
8
 
6
9
  from pyopenapi_gen import IROperation, IRResponse, IRSchema
7
10
 
@@ -84,7 +87,7 @@ class OpenAPIResponseResolver(ResponseTypeResolver):
84
87
 
85
88
  return self.resolve_specific_response(target_response, context)
86
89
 
87
- def _get_primary_response(self, operation: IROperation) -> Optional[IRResponse]:
90
+ def _get_primary_response(self, operation: IROperation) -> IRResponse | None:
88
91
  """Get the primary success response from an operation."""
89
92
  if not operation.responses:
90
93
  return None
@@ -161,9 +164,8 @@ class OpenAPIResponseResolver(ResponseTypeResolver):
161
164
  # For event streams (text/event-stream) or JSON streams
162
165
  is_event_stream = any("event-stream" in ct for ct in content_types)
163
166
  if is_event_stream:
164
- context.add_import("typing", "Dict")
165
167
  context.add_import("typing", "Any")
166
- return ResolvedType(python_type="AsyncIterator[Dict[str, Any]]")
168
+ return ResolvedType(python_type="AsyncIterator[dict[str, Any]]")
167
169
 
168
170
  # For other streaming content, try to resolve the schema
169
171
  schema = self._get_response_schema(response)
@@ -44,6 +44,21 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
44
44
  if hasattr(schema, "ref") and schema.ref:
45
45
  return self._resolve_reference(schema.ref, context, required, resolve_underlying)
46
46
 
47
+ # Handle null schemas (schemas with no type and no generation_name) - resolve to Any inline
48
+ # These are schemas from null schema nodes in the OpenAPI spec
49
+ # IMPORTANT: Don't treat schemas with names that exist in the registry as null schemas
50
+ schema_type = getattr(schema, "type", None)
51
+ if (
52
+ schema_type is None
53
+ and not (hasattr(schema, "generation_name") and schema.generation_name)
54
+ and not (hasattr(schema, "any_of") and schema.any_of)
55
+ and not (hasattr(schema, "one_of") and schema.one_of)
56
+ and not (hasattr(schema, "all_of") and schema.all_of)
57
+ and not (schema.name and schema.name in getattr(self.ref_resolver, "schemas", {}))
58
+ ):
59
+ # This is a null schema - respect the required parameter for optionality
60
+ return self._resolve_null(context, required)
61
+
47
62
  # Handle named schemas with generation_name (fully processed schemas)
48
63
  if schema.name and hasattr(schema, "generation_name") and schema.generation_name:
49
64
  # If resolve_underlying is True, skip named schema resolution for type aliases
@@ -132,7 +147,7 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
132
147
  )
133
148
  elif schema_type == "None" or schema_type is None:
134
149
  logger.info(
135
- "Schema type 'None' detected - likely an optional field or null type. This will be mapped to Optional[Any]."
150
+ "Schema type 'None' detected - likely an optional field or null type. This will be mapped to Any | None."
136
151
  )
137
152
  return self._resolve_null(context, required)
138
153
  elif schema_type and isinstance(schema_type, str):
@@ -143,7 +158,7 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
143
158
  logger.info(" 3. Custom type not supported by OpenAPI (consider using allOf/oneOf/anyOf)")
144
159
  logger.info(f" Location: Check your OpenAPI spec for schemas with type='{schema_type}'")
145
160
 
146
- return self._resolve_any(context)
161
+ return self._resolve_any(context, required)
147
162
 
148
163
  def _resolve_reference(
149
164
  self, ref: str, context: TypeContext, required: bool, resolve_underlying: bool = False
@@ -287,12 +302,13 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
287
302
  return ResolvedType(python_type="bool", is_optional=not required)
288
303
 
289
304
  def _resolve_null(self, context: TypeContext, required: bool) -> ResolvedType:
290
- """Resolve null type."""
291
- # For null types in schemas, we need to import Any for the Optional[Any] pattern
292
- # But the type itself is None for union composition
293
- if not required:
294
- context.add_import("typing", "Any")
295
- return ResolvedType(python_type="None", is_optional=not required)
305
+ """Resolve null type.
306
+
307
+ When a schema has type=null or no type defined (None), we map it to Python's Any type
308
+ because null schemas represent unknown/arbitrary data rather than the None value.
309
+ """
310
+ context.add_import("typing", "Any")
311
+ return ResolvedType(python_type="Any", is_optional=not required)
296
312
 
297
313
  def _resolve_array(
298
314
  self, schema: IRSchema, context: TypeContext, required: bool, resolve_underlying: bool = False
@@ -337,25 +353,25 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
337
353
  # Generic object
338
354
  context.add_import("typing", "Dict")
339
355
  context.add_import("typing", "Any")
340
- return ResolvedType(python_type="Dict[str, Any]", is_optional=not required)
356
+ return ResolvedType(python_type="dict[str, Any]", is_optional=not required)
341
357
 
342
358
  # Object with properties - should be promoted to named type
343
359
  logger.warning("Found object with properties - should be promoted to named type")
344
360
  context.add_import("typing", "Dict")
345
361
  context.add_import("typing", "Any")
346
- return ResolvedType(python_type="Dict[str, Any]", is_optional=not required)
362
+ return ResolvedType(python_type="dict[str, Any]", is_optional=not required)
347
363
 
348
- def _resolve_any(self, context: TypeContext) -> ResolvedType:
364
+ def _resolve_any(self, context: TypeContext, required: bool = True) -> ResolvedType:
349
365
  """Resolve to Any type."""
350
366
  context.add_import("typing", "Any")
351
- return ResolvedType(python_type="Any")
367
+ return ResolvedType(python_type="Any", is_optional=not required)
352
368
 
353
369
  def _resolve_any_of(
354
370
  self, schema: IRSchema, context: TypeContext, required: bool, resolve_underlying: bool = False
355
371
  ) -> ResolvedType:
356
372
  """Resolve anyOf composition to Union type."""
357
373
  if not schema.any_of:
358
- return self._resolve_any(context)
374
+ return self._resolve_any(context, required)
359
375
 
360
376
  resolved_types = []
361
377
  for sub_schema in schema.any_of:
@@ -392,7 +408,7 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
392
408
  # For allOf, we typically need to merge the schemas
393
409
  # For now, we'll use the first schema if it has a clear type
394
410
  if not schema.all_of:
395
- return self._resolve_any(context)
411
+ return self._resolve_any(context, required)
396
412
 
397
413
  # Simple implementation: use the first concrete schema
398
414
  for sub_schema in schema.all_of:
@@ -401,14 +417,14 @@ class OpenAPISchemaResolver(SchemaTypeResolver):
401
417
 
402
418
  # Fallback - if no schema has a concrete type, return Any
403
419
  # Don't recurse into schemas with no type as that causes warnings
404
- return self._resolve_any(context)
420
+ return self._resolve_any(context, required)
405
421
 
406
422
  def _resolve_one_of(
407
423
  self, schema: IRSchema, context: TypeContext, required: bool, resolve_underlying: bool = False
408
424
  ) -> ResolvedType:
409
425
  """Resolve oneOf composition to Union type."""
410
426
  if not schema.one_of:
411
- return self._resolve_any(context)
427
+ return self._resolve_any(context, required)
412
428
 
413
429
  resolved_types = []
414
430
  for sub_schema in schema.one_of:
@@ -1,7 +1,6 @@
1
1
  """Unified type resolution service."""
2
2
 
3
3
  import logging
4
- from typing import Dict, Optional
5
4
 
6
5
  from pyopenapi_gen import IROperation, IRResponse, IRSchema
7
6
  from pyopenapi_gen.context.render_context import RenderContext
@@ -35,7 +34,7 @@ class UnifiedTypeService:
35
34
  and operations to Python type strings.
36
35
  """
37
36
 
38
- def __init__(self, schemas: Dict[str, IRSchema], responses: Optional[Dict[str, IRResponse]] = None):
37
+ def __init__(self, schemas: dict[str, IRSchema], responses: dict[str, IRResponse] | None = None):
39
38
  """
40
39
  Initialize the type service.
41
40
 
@@ -68,7 +67,29 @@ class UnifiedTypeService:
68
67
 
69
68
  type_context = RenderContextAdapter(context)
70
69
  resolved = self.schema_resolver.resolve_schema(schema, type_context, effective_required, resolve_underlying)
71
- return self._format_resolved_type(resolved, context)
70
+
71
+ # DEBUG: Log resolved type before formatting
72
+ if resolved.python_type and "[" in resolved.python_type:
73
+ bracket_count_open = resolved.python_type.count("[")
74
+ bracket_count_close = resolved.python_type.count("]")
75
+ if bracket_count_open != bracket_count_close:
76
+ logger.warning(
77
+ f"BRACKET MISMATCH BEFORE FORMAT: '{resolved.python_type}', "
78
+ f"schema_name: {getattr(schema, 'name', 'anonymous')}, "
79
+ f"schema_type: {getattr(schema, 'type', None)}, "
80
+ f"is_optional: {resolved.is_optional}"
81
+ )
82
+
83
+ formatted = self._format_resolved_type(resolved, context)
84
+
85
+ # DEBUG: Log final formatted type
86
+ if formatted and "[" in formatted:
87
+ bracket_count_open = formatted.count("[")
88
+ bracket_count_close = formatted.count("]")
89
+ if bracket_count_open != bracket_count_close:
90
+ logger.warning(f"BRACKET MISMATCH AFTER FORMAT: '{formatted}', " f"original: '{resolved.python_type}'")
91
+
92
+ return formatted
72
93
 
73
94
  def resolve_operation_response_type(self, operation: IROperation, context: RenderContext) -> str:
74
95
  """
@@ -101,15 +122,40 @@ class UnifiedTypeService:
101
122
  return self._format_resolved_type(resolved, context)
102
123
 
103
124
  def _format_resolved_type(self, resolved: ResolvedType, context: RenderContext | None = None) -> str:
104
- """Format a ResolvedType into a Python type string."""
105
- python_type = resolved.python_type
125
+ """Format a ResolvedType into a Python type string.
106
126
 
107
- if resolved.is_optional and not python_type.startswith("Optional["):
108
- if context:
109
- context.add_import("typing", "Optional")
110
- python_type = f"Optional[{python_type}]"
127
+ Architecture Guarantee: This method produces ONLY modern Python 3.10+ syntax (X | None).
128
+ Optional[X] is NEVER generated - unified type system uses | None exclusively.
129
+ """
130
+ python_type = resolved.python_type
111
131
 
132
+ # SANITY CHECK: Unified system should never produce Optional[X] internally
133
+ if python_type.startswith("Optional["):
134
+ logger.error(
135
+ f"❌ ARCHITECTURE VIOLATION: Resolver produced legacy Optional[X]: {python_type}. "
136
+ f"Unified type system must generate X | None directly. "
137
+ f"This indicates a bug in schema/response/reference resolver."
138
+ )
139
+ # This should never happen in our unified system
140
+ raise ValueError(
141
+ f"Type resolver produced legacy Optional[X] syntax: {python_type}. "
142
+ f"Unified type system must use X | None exclusively."
143
+ )
144
+
145
+ # Quote forward references BEFORE adding | None so we get: "DataSource" | None not "DataSource | None"
112
146
  if resolved.is_forward_ref and not python_type.startswith('"'):
113
147
  python_type = f'"{python_type}"'
114
148
 
149
+ # Add modern | None syntax if needed
150
+ # Modern Python 3.10+ uses | None syntax without needing Optional import
151
+ if resolved.is_optional and not python_type.endswith("| None"):
152
+ python_type = f"{python_type} | None"
153
+
154
+ # DEBUG: Check for malformed type strings
155
+ if python_type.count("[") != python_type.count("]"):
156
+ logger.warning(
157
+ f"MALFORMED TYPE: Bracket mismatch in '{python_type}'. "
158
+ f"Original: '{resolved.python_type}', is_optional: {resolved.is_optional}"
159
+ )
160
+
115
161
  return python_type
@@ -8,7 +8,6 @@ from __future__ import annotations
8
8
 
9
9
  import logging
10
10
  from dataclasses import dataclass
11
- from typing import Dict, Optional
12
11
 
13
12
  from pyopenapi_gen import IROperation, IRResponse, IRSchema
14
13
  from pyopenapi_gen.context.render_context import RenderContext
@@ -28,11 +27,11 @@ class ResponseStrategy:
28
27
  """
29
28
 
30
29
  return_type: str # The Python type for method signature
31
- response_schema: Optional[IRSchema] # The response schema as defined in OpenAPI spec
30
+ response_schema: IRSchema | None # The response schema as defined in OpenAPI spec
32
31
  is_streaming: bool # Whether this is a streaming response
33
32
 
34
33
  # Additional context for code generation
35
- response_ir: Optional[IRResponse] # The original response IR
34
+ response_ir: IRResponse | None # The original response IR
36
35
 
37
36
 
38
37
  class ResponseStrategyResolver:
@@ -43,7 +42,7 @@ class ResponseStrategyResolver:
43
42
  previously spread across multiple components.
44
43
  """
45
44
 
46
- def __init__(self, schemas: Dict[str, IRSchema]):
45
+ def __init__(self, schemas: dict[str, IRSchema]):
47
46
  self.schemas = schemas
48
47
  self.type_service = UnifiedTypeService(schemas)
49
48
 
@@ -89,7 +88,7 @@ class ResponseStrategyResolver:
89
88
  return_type=return_type, response_schema=response_schema, is_streaming=False, response_ir=primary_response
90
89
  )
91
90
 
92
- def _get_primary_response(self, operation: IROperation) -> Optional[IRResponse]:
91
+ def _get_primary_response(self, operation: IROperation) -> IRResponse | None:
93
92
  """Get the primary success response from an operation."""
94
93
  if not operation.responses:
95
94
  return None
@@ -113,7 +112,7 @@ class ResponseStrategyResolver:
113
112
  # First response as fallback
114
113
  return operation.responses[0] if operation.responses else None
115
114
 
116
- def _get_response_schema(self, response: IRResponse) -> Optional[IRSchema]:
115
+ def _get_response_schema(self, response: IRResponse) -> IRSchema | None:
117
116
  """Get the schema from a response's content."""
118
117
  if not response.content:
119
118
  return None
@@ -164,7 +163,7 @@ class ResponseStrategyResolver:
164
163
  context.add_import("typing", "Dict")
165
164
  context.add_import("typing", "Any")
166
165
  return ResponseStrategy(
167
- return_type="AsyncIterator[Dict[str, Any]]",
166
+ return_type="AsyncIterator[dict[str, Any]]",
168
167
  response_schema=None,
169
168
  is_streaming=True,
170
169
  response_ir=response,
@@ -79,7 +79,7 @@ class ClientVisitor:
79
79
  # For now, the client_py_content check in the test looks for this for ApiKeyAuth specifically:
80
80
  context.add_import(f"{context.core_package_name}.auth.plugins", "ApiKeyAuth")
81
81
 
82
- context.add_typing_imports_for_type("Optional[HttpTransport]")
82
+ context.add_typing_imports_for_type("HttpTransport | None")
83
83
  context.add_typing_imports_for_type("Any")
84
84
  context.add_typing_imports_for_type("Dict")
85
85
  # Class definition
@@ -103,7 +103,7 @@ class ClientVisitor:
103
103
  summary = "Async API client with pluggable transport, tag-specific clients, and client-level headers."
104
104
  args: list[tuple[str, str, str]] = [
105
105
  ("config", "ClientConfig", "Client configuration object."),
106
- ("transport", "Optional[HttpTransport]", "Custom HTTP transport (optional)."),
106
+ ("transport", "HttpTransport | None", "Custom HTTP transport (optional)."),
107
107
  ]
108
108
  for tag, class_name, module_name in tag_tuples:
109
109
  args.append((module_name, class_name, f"Client for '{tag}' endpoints."))
@@ -121,9 +121,7 @@ class ClientVisitor:
121
121
  writer.indent() # Back to class indent (1)
122
122
  writer.write_line('"""')
123
123
  # __init__
124
- writer.write_line(
125
- "def __init__(self, config: ClientConfig, transport: Optional[HttpTransport] = None) -> None:"
126
- )
124
+ writer.write_line("def __init__(self, config: ClientConfig, transport: HttpTransport | None = None) -> None:")
127
125
  writer.indent()
128
126
  writer.write_line("self.config = config")
129
127
  writer.write_line(
@@ -133,8 +131,8 @@ class ClientVisitor:
133
131
  writer.write_line("self._base_url: str = str(self.config.base_url)")
134
132
  # Initialize private fields for each tag client
135
133
  for tag, class_name, module_name in tag_tuples:
136
- context.add_typing_imports_for_type(f"Optional[{class_name}]")
137
- writer.write_line(f"self._{module_name}: Optional[{class_name}] = None")
134
+ context.add_typing_imports_for_type(f"{class_name} | None")
135
+ writer.write_line(f"self._{module_name}: {class_name} | None = None")
138
136
  writer.dedent()
139
137
  writer.write_line("")
140
138
  # @property for each tag client
@@ -6,7 +6,7 @@ from __future__ import annotations
6
6
 
7
7
  import logging
8
8
  import textwrap # For _wrap_docstring logic
9
- from typing import TYPE_CHECKING, Any, Dict, Optional
9
+ from typing import TYPE_CHECKING, Any
10
10
 
11
11
  from pyopenapi_gen.core.writers.code_writer import CodeWriter
12
12
  from pyopenapi_gen.core.writers.documentation_writer import DocumentationBlock, DocumentationWriter
@@ -23,8 +23,8 @@ logger = logging.getLogger(__name__)
23
23
  class EndpointDocstringGenerator:
24
24
  """Generates the Python docstring for an endpoint operation."""
25
25
 
26
- def __init__(self, schemas: Optional[Dict[str, Any]] = None) -> None:
27
- self.schemas: Dict[str, Any] = schemas or {}
26
+ def __init__(self, schemas: dict[str, Any] | None = None) -> None:
27
+ self.schemas: dict[str, Any] = schemas or {}
28
28
  self.doc_writer = DocumentationWriter(width=88)
29
29
 
30
30
  def _wrap_docstring(self, prefix: str, text: str, width: int = 88) -> str:
@@ -50,7 +50,7 @@ class EndpointDocstringGenerator:
50
50
  writer: CodeWriter,
51
51
  op: IROperation,
52
52
  context: RenderContext,
53
- primary_content_type: Optional[str],
53
+ primary_content_type: str | None,
54
54
  response_strategy: ResponseStrategy,
55
55
  ) -> None:
56
56
  """Writes the method docstring to the provided CodeWriter."""
@@ -67,10 +67,10 @@ class EndpointDocstringGenerator:
67
67
  body_desc = op.request_body.description or "Request body."
68
68
  # Standardized body parameter names based on content type
69
69
  if primary_content_type == "multipart/form-data":
70
- args.append(("files", "Dict[str, IO[Any]]", body_desc + " (multipart/form-data)"))
70
+ args.append(("files", "dict[str, IO[Any]]", body_desc + " (multipart/form-data)"))
71
71
  elif primary_content_type == "application/x-www-form-urlencoded":
72
- # The type here could be more specific if schema is available, but Dict[str, Any] is a safe default.
73
- args.append(("form_data", "Dict[str, Any]", body_desc + " (x-www-form-urlencoded)"))
72
+ # The type here could be more specific if schema is available, but dict[str, Any] is a safe default.
73
+ args.append(("form_data", "dict[str, Any]", body_desc + " (x-www-form-urlencoded)"))
74
74
  elif primary_content_type == "application/json":
75
75
  body_type = get_request_body_type(op.request_body, context, self.schemas)
76
76
  args.append(("body", body_type, body_desc + " (json)"))
@@ -5,7 +5,7 @@ Helper class for generating the HTTP request call for an endpoint method.
5
5
  from __future__ import annotations
6
6
 
7
7
  import logging
8
- from typing import TYPE_CHECKING, Any, Dict, Optional
8
+ from typing import TYPE_CHECKING, Any
9
9
 
10
10
  from pyopenapi_gen.core.writers.code_writer import CodeWriter
11
11
 
@@ -21,8 +21,8 @@ logger = logging.getLogger(__name__)
21
21
  class EndpointRequestGenerator:
22
22
  """Generates the self._transport.request(...) call for an endpoint method."""
23
23
 
24
- def __init__(self, schemas: Optional[Dict[str, Any]] = None) -> None:
25
- self.schemas: Dict[str, Any] = schemas or {}
24
+ def __init__(self, schemas: dict[str, Any] | None = None) -> None:
25
+ self.schemas: dict[str, Any] = schemas or {}
26
26
 
27
27
  def generate_request_call(
28
28
  self,
@@ -30,8 +30,8 @@ class EndpointRequestGenerator:
30
30
  op: IROperation,
31
31
  context: RenderContext, # Pass context for potential import needs
32
32
  has_header_params: bool,
33
- primary_content_type: Optional[str],
34
- # resolved_body_type: Optional[str], # May not be directly needed here if logic relies on var names
33
+ primary_content_type: str | None,
34
+ # resolved_body_type: str | None, # May not be directly needed here if logic relies on var names
35
35
  ) -> None:
36
36
  """Writes the self._transport.request call to the CodeWriter."""
37
37
  # Logic from EndpointMethodGenerator._write_request