diff --git a/.changeset/fix_nullable_and_required_properties_in_multipart_bodies.md b/.changeset/fix_nullable_and_required_properties_in_multipart_bodies.md new file mode 100644 index 000000000..06c4ea12e --- /dev/null +++ b/.changeset/fix_nullable_and_required_properties_in_multipart_bodies.md @@ -0,0 +1,10 @@ +--- +default: patch +--- + +# Fix nullable and required properties in multipart bodies + +Fixes #926. + +> [!WARNING] +> This change is likely to break custom templates. Multipart body handling has been completely split from JSON bodies. diff --git a/end_to_end_tests/baseline_openapi_3.0.json b/end_to_end_tests/baseline_openapi_3.0.json index 14fdd7c42..11308a286 100644 --- a/end_to_end_tests/baseline_openapi_3.0.json +++ b/end_to_end_tests/baseline_openapi_3.0.json @@ -1415,7 +1415,9 @@ }, "/naming/mixed-case": { "get": { - "tags": ["naming"], + "tags": [ + "naming" + ], "operationId": "mixed_case", "parameters": [ { @@ -1436,30 +1438,32 @@ } ], "responses": { - "200": { - "description": "Successful response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "mixed_case": { - "type": "string" - }, - "mixedCase": { - "type": "string" - } + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "mixed_case": { + "type": "string" + }, + "mixedCase": { + "type": "string" } } } } } + } } } }, "/naming/{hyphen-in-path}": { "get": { - "tags": ["naming"], + "tags": [ + "naming" + ], "operationId": "hyphen_in_path", "parameters": [ { @@ -1863,7 +1867,8 @@ "required": [ "some_file", "some_object", - "some_nullable_object" + "some_nullable_object", + "some_required_number" ], "type": "object", "properties": { @@ -1896,6 +1901,15 @@ "title": "Some Number", "type": "number" }, + "some_nullable_number": { + "title": "Some Nullable Number", + "type": "number", + "nullable": true + }, + "some_required_number": { + "title": "Some Required Number", + "type": "number" + }, "some_array": { "title": "Some Array", "nullable": true, diff --git a/end_to_end_tests/baseline_openapi_3.1.yaml b/end_to_end_tests/baseline_openapi_3.1.yaml index a0e762a95..39a990db2 100644 --- a/end_to_end_tests/baseline_openapi_3.1.yaml +++ b/end_to_end_tests/baseline_openapi_3.1.yaml @@ -1877,7 +1877,8 @@ info: "required": [ "some_file", "some_object", - "some_nullable_object" + "some_nullable_object", + "some_required_number" ], "type": "object", "properties": { @@ -1910,6 +1911,14 @@ info: "title": "Some Number", "type": "number" }, + "some_nullable_number": { + "title": "Some Nullable Number", + "type": [ "number", "null" ] + }, + "some_required_number": { + "title": "Some Number", + "type": "number" + }, "some_array": { "title": "Some Array", "type": [ "array", "null" ], diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py index 20d27d4a6..aa36dba04 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py @@ -32,6 +32,7 @@ class BodyUploadFileTestsUploadPost: """ Attributes: some_file (File): + some_required_number (float): some_object (BodyUploadFileTestsUploadPostSomeObject): some_nullable_object (Union['BodyUploadFileTestsUploadPostSomeNullableObject', None]): some_optional_file (Union[Unset, File]): @@ -39,12 +40,14 @@ class BodyUploadFileTestsUploadPost: a_datetime (Union[Unset, datetime.datetime]): a_date (Union[Unset, datetime.date]): some_number (Union[Unset, float]): + some_nullable_number (Union[None, Unset, float]): some_array (Union[List['AFormData'], None, Unset]): some_optional_object (Union[Unset, BodyUploadFileTestsUploadPostSomeOptionalObject]): some_enum (Union[Unset, DifferentEnum]): An enumeration. """ some_file: File + some_required_number: float some_object: "BodyUploadFileTestsUploadPostSomeObject" some_nullable_object: Union["BodyUploadFileTestsUploadPostSomeNullableObject", None] some_optional_file: Union[Unset, File] = UNSET @@ -52,6 +55,7 @@ class BodyUploadFileTestsUploadPost: a_datetime: Union[Unset, datetime.datetime] = UNSET a_date: Union[Unset, datetime.date] = UNSET some_number: Union[Unset, float] = UNSET + some_nullable_number: Union[None, Unset, float] = UNSET some_array: Union[List["AFormData"], None, Unset] = UNSET some_optional_object: Union[Unset, "BodyUploadFileTestsUploadPostSomeOptionalObject"] = UNSET some_enum: Union[Unset, DifferentEnum] = UNSET @@ -66,6 +70,8 @@ def to_dict(self) -> Dict[str, Any]: some_file = self.some_file.to_tuple() + some_required_number = self.some_required_number + some_object = self.some_object.to_dict() some_nullable_object: Union[Dict[str, Any], None] @@ -90,6 +96,12 @@ def to_dict(self) -> Dict[str, Any]: some_number = self.some_number + some_nullable_number: Union[None, Unset, float] + if isinstance(self.some_nullable_number, Unset): + some_nullable_number = UNSET + else: + some_nullable_number = self.some_nullable_number + some_array: Union[List[Dict[str, Any]], None, Unset] if isinstance(self.some_array, Unset): some_array = UNSET @@ -116,6 +128,7 @@ def to_dict(self) -> Dict[str, Any]: field_dict.update( { "some_file": some_file, + "some_required_number": some_required_number, "some_object": some_object, "some_nullable_object": some_nullable_object, } @@ -130,6 +143,8 @@ def to_dict(self) -> Dict[str, Any]: field_dict["a_date"] = a_date if some_number is not UNSET: field_dict["some_number"] = some_number + if some_nullable_number is not UNSET: + field_dict["some_nullable_number"] = some_nullable_number if some_array is not UNSET: field_dict["some_array"] = some_array if some_optional_object is not UNSET: @@ -142,13 +157,16 @@ def to_dict(self) -> Dict[str, Any]: def to_multipart(self) -> Dict[str, Any]: some_file = self.some_file.to_tuple() + some_required_number = (None, str(self.some_required_number).encode(), "text/plain") + some_object = (None, json.dumps(self.some_object.to_dict()).encode(), "application/json") - some_nullable_object: Union[None, Tuple[None, bytes, str]] + some_nullable_object: Tuple[None, bytes, str] + if isinstance(self.some_nullable_object, BodyUploadFileTestsUploadPostSomeNullableObject): some_nullable_object = (None, json.dumps(self.some_nullable_object.to_dict()).encode(), "application/json") else: - some_nullable_object = self.some_nullable_object + some_nullable_object = (None, str(self.some_nullable_object).encode(), "text/plain") some_optional_file: Union[Unset, FileJsonType] = UNSET if not isinstance(self.some_optional_file, Unset): @@ -174,7 +192,17 @@ def to_multipart(self) -> Dict[str, Any]: else (None, str(self.some_number).encode(), "text/plain") ) - some_array: Union[None, Tuple[None, bytes, str], Unset] + some_nullable_number: Union[Tuple[None, bytes, str], Unset] + + if isinstance(self.some_nullable_number, Unset): + some_nullable_number = UNSET + elif isinstance(self.some_nullable_number, float): + some_nullable_number = (None, str(self.some_nullable_number).encode(), "text/plain") + else: + some_nullable_number = (None, str(self.some_nullable_number).encode(), "text/plain") + + some_array: Union[Tuple[None, bytes, str], Unset] + if isinstance(self.some_array, Unset): some_array = UNSET elif isinstance(self.some_array, list): @@ -183,9 +211,8 @@ def to_multipart(self) -> Dict[str, Any]: some_array_type_0_item = some_array_type_0_item_data.to_dict() _temp_some_array.append(some_array_type_0_item) some_array = (None, json.dumps(_temp_some_array).encode(), "application/json") - else: - some_array = self.some_array + some_array = (None, str(self.some_array).encode(), "text/plain") some_optional_object: Union[Unset, Tuple[None, bytes, str]] = UNSET if not isinstance(self.some_optional_object, Unset): @@ -201,6 +228,7 @@ def to_multipart(self) -> Dict[str, Any]: field_dict.update( { "some_file": some_file, + "some_required_number": some_required_number, "some_object": some_object, "some_nullable_object": some_nullable_object, } @@ -215,6 +243,8 @@ def to_multipart(self) -> Dict[str, Any]: field_dict["a_date"] = a_date if some_number is not UNSET: field_dict["some_number"] = some_number + if some_nullable_number is not UNSET: + field_dict["some_nullable_number"] = some_nullable_number if some_array is not UNSET: field_dict["some_array"] = some_array if some_optional_object is not UNSET: @@ -241,6 +271,8 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: d = src_dict.copy() some_file = File(payload=BytesIO(d.pop("some_file"))) + some_required_number = d.pop("some_required_number") + some_object = BodyUploadFileTestsUploadPostSomeObject.from_dict(d.pop("some_object")) def _parse_some_nullable_object(data: object) -> Union["BodyUploadFileTestsUploadPostSomeNullableObject", None]: @@ -283,6 +315,15 @@ def _parse_some_nullable_object(data: object) -> Union["BodyUploadFileTestsUploa some_number = d.pop("some_number", UNSET) + def _parse_some_nullable_number(data: object) -> Union[None, Unset, float]: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(Union[None, Unset, float], data) + + some_nullable_number = _parse_some_nullable_number(d.pop("some_nullable_number", UNSET)) + def _parse_some_array(data: object) -> Union[List["AFormData"], None, Unset]: if data is None: return data @@ -321,6 +362,7 @@ def _parse_some_array(data: object) -> Union[List["AFormData"], None, Unset]: body_upload_file_tests_upload_post = cls( some_file=some_file, + some_required_number=some_required_number, some_object=some_object, some_nullable_object=some_nullable_object, some_optional_file=some_optional_file, @@ -328,6 +370,7 @@ def _parse_some_array(data: object) -> Union[List["AFormData"], None, Unset]: a_datetime=a_datetime, a_date=a_date, some_number=some_number, + some_nullable_number=some_nullable_number, some_array=some_array, some_optional_object=some_optional_object, some_enum=some_enum, diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_files_body.py b/end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_files_body.py index 1c61d3385..c81dc7636 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_files_body.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_files_body.py @@ -33,9 +33,9 @@ def to_multipart(self) -> Dict[str, Any]: a = self.a if isinstance(self.a, Unset) else (None, str(self.a).encode(), "text/plain") field_dict: Dict[str, Any] = {} - field_dict.update( - {key: (None, str(value).encode(), "text/plain") for key, value in self.additional_properties.items()} - ) + for prop_name, prop in self.additional_properties.items(): + field_dict[prop_name] = (None, str(prop).encode(), "text/plain") + field_dict.update({}) if a is not UNSET: field_dict["a"] = a diff --git a/integration-tests/integration_tests/models/post_body_multipart_body.py b/integration-tests/integration_tests/models/post_body_multipart_body.py index de9992232..f0272a34e 100644 --- a/integration-tests/integration_tests/models/post_body_multipart_body.py +++ b/integration-tests/integration_tests/models/post_body_multipart_body.py @@ -45,9 +45,7 @@ def to_dict(self) -> Dict[str, Any]: return field_dict def to_multipart(self) -> Dict[str, Any]: - a_string = ( - self.a_string if isinstance(self.a_string, Unset) else (None, str(self.a_string).encode(), "text/plain") - ) + a_string = (None, str(self.a_string).encode(), "text/plain") file = self.file.to_tuple() @@ -58,9 +56,9 @@ def to_multipart(self) -> Dict[str, Any]: ) field_dict: Dict[str, Any] = {} - field_dict.update( - {key: (None, str(value).encode(), "text/plain") for key, value in self.additional_properties.items()} - ) + for prop_name, prop in self.additional_properties.items(): + field_dict[prop_name] = (None, str(prop).encode(), "text/plain") + field_dict.update( { "a_string": a_string, diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index bde45ac05..81734e461 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -7,7 +7,9 @@ from ... import Config, utils from ... import schema as oai +from ...utils import PythonIdentifier from ..errors import ParseError, PropertyError +from .any import AnyProperty from .enum_property import EnumProperty from .protocol import PropertyProtocol, Value from .schemas import Class, ReferencePath, Schemas, parse_reference_path @@ -30,7 +32,7 @@ class ModelProperty(PropertyProtocol): optional_properties: list[Property] | None relative_imports: set[str] | None lazy_imports: set[str] | None - additional_properties: bool | Property | None + additional_properties: Property | None _json_type_string: ClassVar[str] = "Dict[str, Any]" template: ClassVar[str] = "model_property.py.jinja" @@ -78,7 +80,7 @@ def build( optional_properties: list[Property] | None = None relative_imports: set[str] | None = None lazy_imports: set[str] | None = None - additional_properties: bool | Property | None = None + additional_properties: Property | None = None if process_properties: data_or_err, schemas = _process_property_data( data=data, schemas=schemas, class_info=class_info, config=config, roots=model_roots @@ -386,6 +388,16 @@ def _add_if_no_conflict(new_prop: Property) -> PropertyError | None: ) +ANY_ADDITIONAL_PROPERTY = AnyProperty.build( + name="additional", + required=True, + default=None, + description="", + python_name=PythonIdentifier(value="additional", prefix=""), + example=None, +) + + def _get_additional_properties( *, schema_additional: None | (bool | (oai.Reference | oai.Schema)), @@ -393,18 +405,20 @@ def _get_additional_properties( class_name: utils.ClassName, config: Config, roots: set[ReferencePath | utils.ClassName], -) -> tuple[bool | (Property | PropertyError), Schemas]: +) -> tuple[Property | None | PropertyError, Schemas]: from . import property_from_data if schema_additional is None: - return True, schemas + return ANY_ADDITIONAL_PROPERTY, schemas if isinstance(schema_additional, bool): - return schema_additional, schemas + if schema_additional: + return ANY_ADDITIONAL_PROPERTY, schemas + return None, schemas if isinstance(schema_additional, oai.Schema) and not any(schema_additional.model_dump().values()): # An empty schema - return True, schemas + return ANY_ADDITIONAL_PROPERTY, schemas additional_properties, schemas = property_from_data( name="AdditionalProperty", @@ -425,7 +439,7 @@ def _process_property_data( class_info: Class, config: Config, roots: set[ReferencePath | utils.ClassName], -) -> tuple[tuple[_PropertyData, bool | Property] | PropertyError, Schemas]: +) -> tuple[tuple[_PropertyData, Property | None] | PropertyError, Schemas]: property_data = _process_properties( data=data, schemas=schemas, class_name=class_info.name, config=config, roots=roots ) @@ -442,7 +456,7 @@ def _process_property_data( ) if isinstance(additional_properties, PropertyError): return additional_properties, schemas - elif isinstance(additional_properties, bool): + elif additional_properties is None: pass else: property_data.relative_imports.update(additional_properties.get_imports(prefix="..")) diff --git a/openapi_python_client/parser/properties/protocol.py b/openapi_python_client/parser/properties/protocol.py index b8237923d..e3f2ceaac 100644 --- a/openapi_python_client/parser/properties/protocol.py +++ b/openapi_python_client/parser/properties/protocol.py @@ -107,6 +107,8 @@ def get_type_string( """ if json: type_string = self.get_base_json_type_string(quoted=quoted) + elif multipart: + type_string = "Tuple[None, bytes, str]" else: type_string = self.get_base_type_string(quoted=quoted) diff --git a/openapi_python_client/templates/endpoint_macros.py.jinja b/openapi_python_client/templates/endpoint_macros.py.jinja index da02b5c4a..79ca33d57 100644 --- a/openapi_python_client/templates/endpoint_macros.py.jinja +++ b/openapi_python_client/templates/endpoint_macros.py.jinja @@ -83,8 +83,8 @@ params = {k: v for k, v in params.items() if v is not UNSET and v is not None} {% macro multipart_body(body, destination) %} {% set property = body.prop %} {% import "property_templates/" + property.template as prop_template %} -{% if prop_template.transform_multipart %} -{{ prop_template.transform_multipart(property, property.python_name, destination) }} +{% if prop_template.transform_multipart_body %} +{{ prop_template.transform_multipart_body(property, property.python_name, destination) }} {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index 0df641ea4..44f5bf148 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -80,10 +80,10 @@ class {{ class_name }}: {% macro _to_dict(multipart=False) %} {% for property in model.required_properties + model.optional_properties %} {% import "property_templates/" + property.template as prop_template %} -{% if prop_template.transform %} -{{ prop_template.transform(property, "self." + property.python_name, property.python_name, multipart=multipart) }} -{% elif multipart %} -{{ property.python_name }} = self.{{ property.python_name }} if isinstance(self.{{ property.python_name }}, Unset) else (None, str(self.{{ property.python_name }}).encode(), "text/plain") +{% if multipart %} +{{ prop_template.transform_multipart(property, "self." + property.python_name, property.python_name) }} +{% elif prop_template.transform %} +{{ prop_template.transform(property=property, source="self." + property.python_name, destination=property.python_name) }} {% else %} {{ property.python_name }} = self.{{ property.python_name }} {% endif %} @@ -92,19 +92,13 @@ class {{ class_name }}: field_dict: Dict[str, Any] = {} {% if model.additional_properties %} -{% if model.additional_properties.template %}{# Can be a bool instead of an object #} - {% import "property_templates/" + model.additional_properties.template as prop_template %} -{% else %} - {% set prop_template = None %} -{% endif %} -{% if prop_template and prop_template.transform %} +{% import "property_templates/" + model.additional_properties.template as prop_template %} +{% if multipart %} for prop_name, prop in self.additional_properties.items(): - {{ prop_template.transform(model.additional_properties, "prop", "field_dict[prop_name]", multipart=multipart, declare_type=false) | indent(4) }} -{% elif multipart %} -field_dict.update({ - key: (None, str(value).encode(), "text/plain") - for key, value in self.additional_properties.items() -}) + {{ prop_template.transform_multipart(model.additional_properties, "prop", "field_dict[prop_name]") | indent(4) }} +{% elif prop_template.transform %} +for prop_name, prop in self.additional_properties.items(): + {{ prop_template.transform(model.additional_properties, "prop", "field_dict[prop_name]", declare_type=false) | indent(4) }} {% else %} field_dict.update(self.additional_properties) {% endif %} diff --git a/openapi_python_client/templates/property_templates/any_property.py.jinja b/openapi_python_client/templates/property_templates/any_property.py.jinja index e69de29bb..25a16516a 100644 --- a/openapi_python_client/templates/property_templates/any_property.py.jinja +++ b/openapi_python_client/templates/property_templates/any_property.py.jinja @@ -0,0 +1,7 @@ +{% macro transform_multipart(property, source, destination) %} +{% if not property.required %} +{{ destination }} = {{source}} if isinstance({{source}}, Unset) else (None, str({{source}}).encode(), "text/plain") +{% else %} +{{ destination }} = (None, str({{ source }}).encode(), "text/plain") +{% endif %} +{% endmacro %} \ No newline at end of file diff --git a/openapi_python_client/templates/property_templates/boolean_property.py.jinja b/openapi_python_client/templates/property_templates/boolean_property.py.jinja index 3b16b7d20..52fda08e1 100644 --- a/openapi_python_client/templates/property_templates/boolean_property.py.jinja +++ b/openapi_python_client/templates/property_templates/boolean_property.py.jinja @@ -1,3 +1,11 @@ {% macro transform_header(source) %} "true" if {{ source }} else "false" {% endmacro %} + +{% macro transform_multipart(property, source, destination) %} +{% if not property.required %} +{{ destination }} = {{source}} if isinstance({{source}}, Unset) else (None, str({{source}}).encode(), "text/plain") +{% else %} +{{ destination }} = (None, str({{ source }}).encode(), "text/plain") +{% endif %} +{% endmacro %} diff --git a/openapi_python_client/templates/property_templates/date_property.py.jinja b/openapi_python_client/templates/property_templates/date_property.py.jinja index 5a3fdeafc..b5da665f7 100644 --- a/openapi_python_client/templates/property_templates/date_property.py.jinja +++ b/openapi_python_client/templates/property_templates/date_property.py.jinja @@ -10,17 +10,13 @@ isoparse({{ source }}).date() {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, str){% endmacro %} -{% macro transform(property, source, destination, declare_type=True, multipart=False) %} +{% macro transform(property, source, destination, declare_type=True) %} {% set transformed = source + ".isoformat()" %} -{% if multipart %}{# Multipart data must be bytes, not str #} -{% set transformed = transformed + ".encode()" %} -{% endif %} {% if property.required %} {{ destination }} = {{ transformed }} {%- else %} {% if declare_type %} {% set type_annotation = property.get_type_string(json=True) %} -{% if multipart %}{% set type_annotation = type_annotation | replace("str", "bytes") %}{% endif %} {{ destination }}: {{ type_annotation }} = UNSET {% else %} {{ destination }} = UNSET @@ -29,3 +25,15 @@ if not isinstance({{ source }}, Unset): {{ destination }} = {{ transformed }} {%- endif %} {% endmacro %} + +{% macro transform_multipart(property, source, destination) %} +{% set transformed = source + ".isoformat().encode()" %} +{% if property.required %} +{{ destination }} = {{ transformed }} +{%- else %} +{% set type_annotation = property.get_type_string(json=True) | replace("str", "bytes") %} +{{ destination }}: {{ type_annotation }} = UNSET +if not isinstance({{ source }}, Unset): + {{ destination }} = {{ transformed }} +{%- endif %} +{% endmacro %} diff --git a/openapi_python_client/templates/property_templates/datetime_property.py.jinja b/openapi_python_client/templates/property_templates/datetime_property.py.jinja index 2ff54f4dc..32e3f2ee6 100644 --- a/openapi_python_client/templates/property_templates/datetime_property.py.jinja +++ b/openapi_python_client/templates/property_templates/datetime_property.py.jinja @@ -10,17 +10,13 @@ isoparse({{ source }}) {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, str){% endmacro %} -{% macro transform(property, source, destination, declare_type=True, multipart=False) %} +{% macro transform(property, source, destination, declare_type=True) %} {% set transformed = source + ".isoformat()" %} -{% if multipart %}{# Multipart data must be bytes, not str #} -{% set transformed = transformed + ".encode()" %} -{% endif %} {% if property.required %} {{ destination }} = {{ transformed }} {%- else %} {% if declare_type %} {% set type_annotation = property.get_type_string(json=True) %} -{% if multipart %}{% set type_annotation = type_annotation | replace("str", "bytes") %}{% endif %} {{ destination }}: {{ type_annotation }} = UNSET {% else %} {{ destination }} = UNSET @@ -29,3 +25,15 @@ if not isinstance({{ source }}, Unset): {{ destination }} = {{ transformed }} {%- endif %} {% endmacro %} + +{% macro transform_multipart(property, source, destination) %} +{% set transformed = source + ".isoformat().encode()" %} +{% if property.required %} +{{ destination }} = {{ transformed }} +{%- else %} +{% set type_annotation = property.get_type_string(json=True) | replace("str", "bytes") %} +{{ destination }}: {{ type_annotation }} = UNSET +if not isinstance({{ source }}, Unset): + {{ destination }} = {{ transformed }} +{%- endif %} +{% endmacro %} diff --git a/openapi_python_client/templates/property_templates/enum_property.py.jinja b/openapi_python_client/templates/property_templates/enum_property.py.jinja index d01137f03..ea9b66a51 100644 --- a/openapi_python_client/templates/property_templates/enum_property.py.jinja +++ b/openapi_python_client/templates/property_templates/enum_property.py.jinja @@ -13,10 +13,6 @@ {% macro transform(property, source, destination, declare_type=True, multipart=False) %} {% set transformed = source + ".value" %} {% set type_string = property.get_type_string(json=True) %} -{% if multipart %} - {% set transformed = "(None, str(" + transformed + ").encode(), \"text/plain\")" %} - {% set type_string = "Union[Unset, Tuple[None, bytes, str]]" %} -{% endif %} {% if property.required %} {{ destination }} = {{ transformed }} {%- else %} @@ -26,6 +22,18 @@ if not isinstance({{ source }}, Unset): {% endif %} {% endmacro %} +{% macro transform_multipart(property, source, destination) %} +{% set transformed = "(None, str(" + source + ".value" + ").encode(), \"text/plain\")" %} +{% set type_string = "Union[Unset, Tuple[None, bytes, str]]" %} +{% if property.required %} +{{ destination }} = {{ transformed }} +{%- else %} +{{ destination }}: {{ type_string }} = UNSET +if not isinstance({{ source }}, Unset): + {{ destination }} = {{ transformed }} +{% endif %} +{% endmacro %} + {% macro transform_header(source) %} str({{ source }}) {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/file_property.py.jinja b/openapi_python_client/templates/property_templates/file_property.py.jinja index c19a068c5..8d27ae617 100644 --- a/openapi_python_client/templates/property_templates/file_property.py.jinja +++ b/openapi_python_client/templates/property_templates/file_property.py.jinja @@ -12,7 +12,7 @@ File( {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, bytes){% endmacro %} -{% macro transform(property, source, destination, declare_type=True, multipart=False) %} +{% macro transform(property, source, destination, declare_type=True) %} {% if property.required %} {{ destination }} = {{ source }}.to_tuple() {% else %} @@ -21,3 +21,13 @@ if not isinstance({{ source }}, Unset): {{ destination }} = {{ source }}.to_tuple() {% endif %} {% endmacro %} + +{% macro transform_multipart(property, source, destination) %} +{% if property.required %} +{{ destination }} = {{ source }}.to_tuple() +{% else %} +{{ destination }}: {{ property.get_type_string(json=True) }} = UNSET +if not isinstance({{ source }}, Unset): + {{ destination }} = {{ source }}.to_tuple() +{% endif %} +{% endmacro %} diff --git a/openapi_python_client/templates/property_templates/float_property.py.jinja b/openapi_python_client/templates/property_templates/float_property.py.jinja index 0d433c22e..12ffb0fb4 100644 --- a/openapi_python_client/templates/property_templates/float_property.py.jinja +++ b/openapi_python_client/templates/property_templates/float_property.py.jinja @@ -1,3 +1,11 @@ {% macro transform_header(source) %} str({{ source }}) {% endmacro %} + +{% macro transform_multipart(property, source, destination) %} +{% if not property.required %} +{{ destination }} = {{source}} if isinstance({{source}}, Unset) else (None, str({{source}}).encode(), "text/plain") +{% else %} +{{ destination }} = (None, str({{ source }}).encode(), "text/plain") +{% endif %} +{% endmacro %} diff --git a/openapi_python_client/templates/property_templates/int_property.py.jinja b/openapi_python_client/templates/property_templates/int_property.py.jinja index 0d433c22e..12ffb0fb4 100644 --- a/openapi_python_client/templates/property_templates/int_property.py.jinja +++ b/openapi_python_client/templates/property_templates/int_property.py.jinja @@ -1,3 +1,11 @@ {% macro transform_header(source) %} str({{ source }}) {% endmacro %} + +{% macro transform_multipart(property, source, destination) %} +{% if not property.required %} +{{ destination }} = {{source}} if isinstance({{source}}, Unset) else (None, str({{source}}).encode(), "text/plain") +{% else %} +{{ destination }} = (None, str({{ source }}).encode(), "text/plain") +{% endif %} +{% endmacro %} diff --git a/openapi_python_client/templates/property_templates/list_property.py.jinja b/openapi_python_client/templates/property_templates/list_property.py.jinja index 0e5d1b5e3..4b51c8e01 100644 --- a/openapi_python_client/templates/property_templates/list_property.py.jinja +++ b/openapi_python_client/templates/property_templates/list_property.py.jinja @@ -40,24 +40,38 @@ for {{ inner_source }} in {{ source }}: {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, list){% endmacro %} -{% macro transform(property, source, destination, declare_type=True, multipart=False, transform_method="to_dict") %} +{% macro transform(property, source, destination, declare_type=True) %} {% set inner_property = property.inner_property %} -{% if multipart %} - {% set type_string = "Union[Unset, Tuple[None, bytes, str]]" %} -{% else %} - {% set type_string = property.get_type_string(json=True) %} -{% endif %} +{% set type_string = property.get_type_string(json=True) %} {% if property.required %} -{{ _transform(property, source, destination, multipart, transform_method) }} +{{ _transform(property, source, destination, False, "to_dict") }} {% else %} {{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET if not isinstance({{ source }}, Unset): - {{ _transform(property, source, destination, multipart, transform_method) | indent(4)}} + {{ _transform(property, source, destination, False, "to_dict") | indent(4)}} {% endif %} - - {% endmacro %} {% macro transform_multipart(property, source, destination) %} -{{ transform(property, source, destination, transform_method="to_multipart") }} +{% set inner_property = property.inner_property %} +{% set type_string = "Union[Unset, Tuple[None, bytes, str]]" %} +{% if property.required %} +{{ _transform(property, source, destination, True, "to_dict") }} +{% else %} +{{ destination }}: {{ type_string }} = UNSET +if not isinstance({{ source }}, Unset): +{{ _transform(property, source, destination, True, "to_dict") | indent(4)}} +{% endif %} +{% endmacro %} + +{% macro transform_multipart_body(property, source, destination) %} +{% set inner_property = property.inner_property %} +{% set type_string = property.get_type_string(json=True) %} +{% if property.required %} +{{ _transform(property, source, destination, False, "to_multipart") }} +{% else %} +{{ destination }}: {{ type_string }} = UNSET +if not isinstance({{ source }}, Unset): + {{ _transform(property, source, destination, False, "to_multipart") | indent(4)}} +{% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/model_property.py.jinja b/openapi_python_client/templates/property_templates/model_property.py.jinja index e7f779563..308b7478b 100644 --- a/openapi_python_client/templates/property_templates/model_property.py.jinja +++ b/openapi_python_client/templates/property_templates/model_property.py.jinja @@ -10,14 +10,9 @@ {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, dict){% endmacro %} -{% macro transform(property, source, destination, declare_type=True, multipart=False, transform_method="to_dict") %} -{% set transformed = source + "." + transform_method + "()" %} -{% if multipart %} - {% set transformed = "(None, json.dumps(" + transformed + ").encode(), 'application/json')" %} - {% set type_string = property.get_type_string(multipart=True) %} -{% else %} - {% set type_string = property.get_type_string(json=True) %} -{% endif %} +{% macro transform(property, source, destination, declare_type=True) %} +{% set transformed = source + ".to_dict()" %} +{% set type_string = property.get_type_string(json=True) %} {% if property.required %} {{ destination }} = {{ transformed }} {%- else %} @@ -27,6 +22,26 @@ if not isinstance({{ source }}, Unset): {%- endif %} {% endmacro %} +{% macro transform_multipart_body(property, source, destination) %} +{% set transformed = source + ".to_multipart()" %} +{% set type_string = property.get_type_string(multipart=True) %} +{% if property.required %} +{{ destination }} = {{ transformed }} +{%- else %} +{{ destination }}: {{ type_string }} = UNSET +if not isinstance({{ source }}, Unset): + {{ destination }} = {{ transformed }} +{%- endif %} +{% endmacro %} + {% macro transform_multipart(property, source, destination) %} -{{ transform(property, source, destination, transform_method="to_multipart") }} +{% set transformed = "(None, json.dumps(" + source + ".to_dict()" + ").encode(), 'application/json')" %} +{% set type_string = property.get_type_string(multipart=True) %} +{% if property.required %} +{{ destination }} = {{ transformed }} +{%- else %} +{{ destination }}: {{ type_string }} = UNSET +if not isinstance({{ source }}, Unset): + {{ destination }} = {{ transformed }} +{%- endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/union_property.py.jinja b/openapi_python_client/templates/property_templates/union_property.py.jinja index b8ab1962d..dbf7ee9dc 100644 --- a/openapi_python_client/templates/property_templates/union_property.py.jinja +++ b/openapi_python_client/templates/property_templates/union_property.py.jinja @@ -39,9 +39,9 @@ def _parse_{{ property.python_name }}(data: object) -> {{ property.get_type_stri {{ property.python_name }} = _parse_{{ property.python_name }}({{ source }}) {% endmacro %} -{% macro transform(property, source, destination, declare_type=True, multipart=False) %} +{% macro transform(property, source, destination, declare_type=True) %} {% set ns = namespace(contains_properties_without_transform = false, contains_modified_properties = not property.required, has_if = false) %} -{% if declare_type %}{{ destination }}: {{ property.get_type_string(json=not multipart, multipart=multipart) }}{% endif %} +{% if declare_type %}{{ destination }}: {{ property.get_type_string(json=True, multipart=False) }}{% endif %} {% if not property.required %} if isinstance({{ source }}, Unset): @@ -64,7 +64,7 @@ elif isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): {% else %} else: {% endif %} - {{ inner_template.transform(inner_property, source, destination, declare_type=False, multipart=multipart) | indent(4) }} + {{ inner_template.transform(inner_property, source, destination, declare_type=False) | indent(4) }} {% endfor %} {% if ns.contains_properties_without_transform and ns.contains_modified_properties %} else: @@ -73,3 +73,27 @@ else: {{ destination }} = {{ source }} {%- endif %} {% endmacro %} + + +{% macro transform_multipart(property, source, destination) %} +{% set ns = namespace(has_if = false) %} +{{ destination }}: {{ property.get_type_string(json=False, multipart=True) }} + +{% if not property.required %} +if isinstance({{ source }}, Unset): + {{ destination }} = UNSET + {% set ns.has_if = true %} +{% endif %} +{% for inner_property in property.inner_properties %} +{% if not ns.has_if %} +if isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): +{% set ns.has_if = true %} +{% elif not loop.last %} +elif isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): +{% else %} +else: +{% endif %} +{% import "property_templates/" + inner_property.template as inner_template %} + {{ inner_template.transform_multipart(inner_property, source, destination) | indent(4) | trim }} +{% endfor %} +{% endmacro %} diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py index 917582042..a7d7c1f23 100644 --- a/tests/test_parser/test_properties/test_model_property.py +++ b/tests/test_parser/test_properties/test_model_property.py @@ -6,7 +6,7 @@ import openapi_python_client.schema as oai from openapi_python_client.parser.errors import PropertyError from openapi_python_client.parser.properties import Schemas, StringProperty -from openapi_python_client.parser.properties.model_property import _process_properties +from openapi_python_client.parser.properties.model_property import ANY_ADDITIONAL_PROPERTY, _process_properties MODULE_NAME = "openapi_python_client.parser.properties.model_property" @@ -70,10 +70,10 @@ class TestBuild: @pytest.mark.parametrize( "additional_properties_schema, expected_additional_properties", [ - (True, True), - (oai.Schema.model_construct(), True), - (None, True), - (False, False), + (True, ANY_ADDITIONAL_PROPERTY), + (oai.Schema.model_construct(), ANY_ADDITIONAL_PROPERTY), + (None, ANY_ADDITIONAL_PROPERTY), + (False, None), ( oai.Schema.model_construct(type="string"), StringProperty( @@ -163,7 +163,7 @@ def test_happy_path(self, model_property_factory, string_property_factory, date_ "from typing import Union", }, lazy_imports=set(), - additional_properties=True, + additional_properties=ANY_ADDITIONAL_PROPERTY, ) def test_model_name_conflict(self, config):