pyopenapi-gen 0.14.0__py3-none-any.whl → 0.14.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyopenapi_gen/cli.py +3 -3
- pyopenapi_gen/context/import_collector.py +10 -10
- pyopenapi_gen/context/render_context.py +13 -13
- pyopenapi_gen/core/auth/plugins.py +7 -7
- pyopenapi_gen/core/http_status_codes.py +2 -4
- pyopenapi_gen/core/http_transport.py +19 -19
- pyopenapi_gen/core/loader/operations/parser.py +2 -2
- pyopenapi_gen/core/loader/operations/request_body.py +3 -3
- pyopenapi_gen/core/loader/parameters/parser.py +3 -3
- pyopenapi_gen/core/loader/responses/parser.py +2 -2
- pyopenapi_gen/core/loader/schemas/extractor.py +4 -4
- pyopenapi_gen/core/pagination.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +2 -2
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +3 -3
- pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +2 -2
- pyopenapi_gen/core/parsing/common/type_parser.py +2 -3
- pyopenapi_gen/core/parsing/context.py +10 -10
- pyopenapi_gen/core/parsing/cycle_helpers.py +5 -2
- pyopenapi_gen/core/parsing/keywords/all_of_parser.py +5 -5
- pyopenapi_gen/core/parsing/keywords/any_of_parser.py +4 -4
- pyopenapi_gen/core/parsing/keywords/array_items_parser.py +4 -4
- pyopenapi_gen/core/parsing/keywords/one_of_parser.py +4 -4
- pyopenapi_gen/core/parsing/keywords/properties_parser.py +5 -5
- pyopenapi_gen/core/parsing/schema_finalizer.py +15 -15
- pyopenapi_gen/core/parsing/schema_parser.py +44 -25
- pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +4 -4
- pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +7 -4
- pyopenapi_gen/core/parsing/unified_cycle_detection.py +10 -10
- pyopenapi_gen/core/schemas.py +10 -10
- pyopenapi_gen/core/streaming_helpers.py +5 -7
- pyopenapi_gen/core/telemetry.py +4 -4
- pyopenapi_gen/core/utils.py +7 -7
- pyopenapi_gen/core/writers/code_writer.py +2 -2
- pyopenapi_gen/core/writers/documentation_writer.py +18 -18
- pyopenapi_gen/core/writers/line_writer.py +3 -3
- pyopenapi_gen/core/writers/python_construct_renderer.py +10 -10
- pyopenapi_gen/emit/models_emitter.py +2 -2
- pyopenapi_gen/emitters/core_emitter.py +3 -5
- pyopenapi_gen/emitters/endpoints_emitter.py +12 -12
- pyopenapi_gen/emitters/exceptions_emitter.py +4 -3
- pyopenapi_gen/emitters/models_emitter.py +6 -6
- pyopenapi_gen/generator/client_generator.py +6 -6
- pyopenapi_gen/helpers/endpoint_utils.py +16 -18
- pyopenapi_gen/helpers/type_cleaner.py +66 -53
- pyopenapi_gen/helpers/type_helper.py +7 -7
- pyopenapi_gen/helpers/type_resolution/array_resolver.py +4 -4
- pyopenapi_gen/helpers/type_resolution/composition_resolver.py +5 -5
- pyopenapi_gen/helpers/type_resolution/finalizer.py +38 -22
- pyopenapi_gen/helpers/type_resolution/named_resolver.py +4 -5
- pyopenapi_gen/helpers/type_resolution/object_resolver.py +11 -11
- pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +1 -2
- pyopenapi_gen/helpers/type_resolution/resolver.py +2 -3
- pyopenapi_gen/ir.py +32 -34
- pyopenapi_gen/types/contracts/protocols.py +5 -5
- pyopenapi_gen/types/contracts/types.py +2 -3
- pyopenapi_gen/types/resolvers/reference_resolver.py +4 -4
- pyopenapi_gen/types/resolvers/response_resolver.py +6 -4
- pyopenapi_gen/types/resolvers/schema_resolver.py +32 -16
- pyopenapi_gen/types/services/type_service.py +55 -9
- pyopenapi_gen/types/strategies/response_strategy.py +6 -7
- pyopenapi_gen/visit/client_visitor.py +5 -7
- pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +7 -7
- pyopenapi_gen/visit/endpoint/generators/request_generator.py +5 -5
- pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +38 -17
- pyopenapi_gen/visit/endpoint/generators/signature_generator.py +4 -4
- pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +17 -17
- pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +8 -8
- pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +13 -13
- pyopenapi_gen/visit/model/alias_generator.py +1 -4
- pyopenapi_gen/visit/model/dataclass_generator.py +139 -10
- pyopenapi_gen/visit/model/model_visitor.py +2 -3
- pyopenapi_gen/visit/visitor.py +3 -3
- {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/METADATA +1 -1
- pyopenapi_gen-0.14.1.dist-info/RECORD +132 -0
- pyopenapi_gen-0.14.0.dist-info/RECORD +0 -132
- {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/WHEEL +0 -0
- {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/entry_points.txt +0 -0
- {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/licenses/LICENSE +0 -0
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,
|
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:
|
20
|
-
type:
|
21
|
-
format:
|
22
|
-
description:
|
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:
|
25
|
-
items:
|
26
|
-
enum:
|
27
|
-
default:
|
28
|
-
example:
|
29
|
-
additional_properties:
|
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:
|
32
|
-
one_of:
|
33
|
-
all_of:
|
34
|
-
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:
|
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:
|
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:
|
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:
|
59
|
-
final_module_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:
|
123
|
-
# example:
|
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:
|
131
|
-
content:
|
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:
|
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:
|
140
|
-
description:
|
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:
|
149
|
-
description:
|
146
|
+
summary: str | None
|
147
|
+
description: str | None
|
150
148
|
parameters: List[IRParameter] = field(default_factory=list)
|
151
|
-
request_body:
|
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:
|
161
|
-
schemas:
|
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
|
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:
|
29
|
-
responses:
|
28
|
+
schemas: dict[str, IRSchema]
|
29
|
+
responses: dict[str, IRResponse]
|
30
30
|
|
31
31
|
@abstractmethod
|
32
|
-
def resolve_ref(self, ref: str) ->
|
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) ->
|
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:
|
20
|
-
import_name:
|
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
|
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:
|
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) ->
|
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) ->
|
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
|
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) ->
|
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[
|
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
|
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
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
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="
|
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="
|
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:
|
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
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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:
|
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:
|
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:
|
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) ->
|
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) ->
|
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[
|
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("
|
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", "
|
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"
|
137
|
-
writer.write_line(f"self._{module_name}:
|
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
|
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:
|
27
|
-
self.schemas:
|
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:
|
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", "
|
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
|
73
|
-
args.append(("form_data", "
|
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
|
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:
|
25
|
-
self.schemas:
|
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:
|
34
|
-
# resolved_body_type:
|
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
|