From 083c2b2285d4221a74409fcde845f1fc2aff769b Mon Sep 17 00:00:00 2001 From: Constantinos Symeonides Date: Wed, 31 Mar 2021 16:01:39 +0100 Subject: [PATCH 01/10] fix: Detect File fields correctly --- .../api/tests/upload_file_tests_upload_post.py | 4 ++-- .../golden-record/my_test_api_client/types.py | 9 +++++++-- openapi_python_client/templates/endpoint_module.py.jinja | 4 ++-- openapi_python_client/templates/types.py.jinja | 9 +++++++-- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py index 7148ce5f3..6b49d3284 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py @@ -5,7 +5,7 @@ from ...client import Client from ...models.body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost from ...models.http_validation_error import HTTPValidationError -from ...types import UNSET, File, Response, Unset +from ...types import UNSET, Response, Unset, is_file def _get_kwargs( @@ -25,7 +25,7 @@ def _get_kwargs( files = {} data = {} for key, value in multipart_data.to_dict().items(): - if isinstance(value, File): + if is_file(value): files[key] = value else: data[key] = value diff --git a/end_to_end_tests/golden-record/my_test_api_client/types.py b/end_to_end_tests/golden-record/my_test_api_client/types.py index 2b1cfc5b8..85ce7fdd3 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/types.py +++ b/end_to_end_tests/golden-record/my_test_api_client/types.py @@ -1,5 +1,6 @@ """ Contains some shared types for properties """ -from typing import BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union +from io import IOBase +from typing import Any, BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union import attr @@ -14,6 +15,10 @@ def __bool__(self) -> bool: FileJsonType = Tuple[Optional[str], Union[BinaryIO, TextIO], Optional[str]] +def is_file(value: Any) -> bool: + return isinstance(value, tuple) and len(value) == 3 and isinstance(value[1], IOBase) + + @attr.s(auto_attribs=True) class File: """Contains information for file uploads""" @@ -40,4 +45,4 @@ class Response(Generic[T]): parsed: Optional[T] -__all__ = ["File", "Response"] +__all__ = ["File", "Response", "is_file"] diff --git a/openapi_python_client/templates/endpoint_module.py.jinja b/openapi_python_client/templates/endpoint_module.py.jinja index 687705def..9702dfc99 100644 --- a/openapi_python_client/templates/endpoint_module.py.jinja +++ b/openapi_python_client/templates/endpoint_module.py.jinja @@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional, Union, cast import httpx from ...client import AuthenticatedClient, Client -from ...types import Response, UNSET{% if endpoint.multipart_body_class %}, File {% endif %} +from ...types import Response, UNSET{% if endpoint.multipart_body_class %}, is_file {% endif %} {% for relative in endpoint.relative_imports %} {{ relative }} @@ -39,7 +39,7 @@ def _get_kwargs( files = {} data = {} for key, value in multipart_data.to_dict().items(): - if isinstance(value, File): + if is_file(value): files[key] = value else: data[key] = value diff --git a/openapi_python_client/templates/types.py.jinja b/openapi_python_client/templates/types.py.jinja index 116054226..669816c8f 100644 --- a/openapi_python_client/templates/types.py.jinja +++ b/openapi_python_client/templates/types.py.jinja @@ -1,5 +1,6 @@ """ Contains some shared types for properties """ -from typing import BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union +from io import IOBase +from typing import Any, BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union import attr @@ -15,6 +16,10 @@ UNSET: Unset = Unset() FileJsonType = Tuple[Optional[str], Union[BinaryIO, TextIO], Optional[str]] +def is_file(value: Any) -> bool: + return isinstance(value, tuple) and len(value) == 3 and isinstance(value[1], IOBase) + + @attr.s(auto_attribs=True) class File: """ Contains information for file uploads """ @@ -41,4 +46,4 @@ class Response(Generic[T]): parsed: Optional[T] -__all__ = ["File", "Response"] +__all__ = ["File", "Response", "is_file"] From 7e2d73f2c2b9240074937c2ce36277808c5daab2 Mon Sep 17 00:00:00 2001 From: Constantinos Symeonides Date: Wed, 31 Mar 2021 16:12:40 +0100 Subject: [PATCH 02/10] fix: Optional File field caused missing import error --- .../models/body_upload_file_tests_upload_post.py | 15 ++++++++++++++- .../golden-record/my_test_api_client/types.py | 2 +- end_to_end_tests/openapi.json | 5 +++++ .../parser/properties/__init__.py | 4 ++-- openapi_python_client/templates/types.py.jinja | 2 +- tests/test_parser/test_properties/test_init.py | 6 +++--- 6 files changed, 26 insertions(+), 8 deletions(-) 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 d2d263353..a708a1d0b 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 @@ -3,7 +3,7 @@ import attr -from ..types import UNSET, File, Unset +from ..types import UNSET, File, FileJsonType, Unset T = TypeVar("T", bound="BodyUploadFileTestsUploadPost") @@ -13,11 +13,16 @@ class BodyUploadFileTestsUploadPost: """ """ some_file: File + some_optional_file: Union[Unset, File] = UNSET some_string: Union[Unset, str] = "some_default_string" def to_dict(self) -> Dict[str, Any]: some_file = self.some_file.to_tuple() + some_optional_file: Union[Unset, FileJsonType] = UNSET + if not isinstance(self.some_optional_file, Unset): + some_optional_file = self.some_optional_file.to_tuple() + some_string = self.some_string field_dict: Dict[str, Any] = {} @@ -26,6 +31,8 @@ def to_dict(self) -> Dict[str, Any]: "some_file": some_file, } ) + if some_optional_file is not UNSET: + field_dict["some_optional_file"] = some_optional_file if some_string is not UNSET: field_dict["some_string"] = some_string @@ -36,10 +43,16 @@ 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_optional_file: Union[Unset, File] = UNSET + _some_optional_file = d.pop("some_optional_file", UNSET) + if not isinstance(_some_optional_file, Unset): + some_optional_file = File(payload=BytesIO(_some_optional_file)) + some_string = d.pop("some_string", UNSET) body_upload_file_tests_upload_post = cls( some_file=some_file, + some_optional_file=some_optional_file, some_string=some_string, ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/types.py b/end_to_end_tests/golden-record/my_test_api_client/types.py index 85ce7fdd3..668dfec37 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/types.py +++ b/end_to_end_tests/golden-record/my_test_api_client/types.py @@ -45,4 +45,4 @@ class Response(Generic[T]): parsed: Optional[T] -__all__ = ["File", "Response", "is_file"] +__all__ = ["File", "Response", "is_file", "FileJsonType"] diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index bbd227358..b3cfcd528 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -1063,6 +1063,11 @@ "type": "string", "format": "binary" }, + "some_optional_file": { + "title": "Some Optional File", + "type": "string", + "format": "binary" + }, "some_string": { "title": "Some String", "type": "string", diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index f6ec49d2d..243554fb6 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -94,7 +94,7 @@ class FileProperty(Property): _type_string: ClassVar[str] = "File" # Return type of File.to_tuple() - _json_type_string: ClassVar[str] = "Tuple[Optional[str], Union[BinaryIO, TextIO], Optional[str]]" + _json_type_string: ClassVar[str] = "FileJsonType" template: ClassVar[str] = "file_property.py.jinja" def get_imports(self, *, prefix: str) -> Set[str]: @@ -106,7 +106,7 @@ def get_imports(self, *, prefix: str) -> Set[str]: back to the root of the generated client. """ imports = super().get_imports(prefix=prefix) - imports.update({f"from {prefix}types import File", "from io import BytesIO"}) + imports.update({f"from {prefix}types import File, FileJsonType", "from io import BytesIO"}) return imports diff --git a/openapi_python_client/templates/types.py.jinja b/openapi_python_client/templates/types.py.jinja index 669816c8f..54293eaa0 100644 --- a/openapi_python_client/templates/types.py.jinja +++ b/openapi_python_client/templates/types.py.jinja @@ -46,4 +46,4 @@ class Response(Generic[T]): parsed: Optional[T] -__all__ = ["File", "Response", "is_file"] +__all__ = ["File", "Response", "is_file", "FileJsonType"] diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index 1f5646d74..57cf3ad22 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -166,13 +166,13 @@ def test_get_imports(self): p = FileProperty(name="test", required=True, default=None, nullable=False) assert p.get_imports(prefix=prefix) == { "from io import BytesIO", - "from ...types import File", + "from ...types import File, FileJsonType", } p = FileProperty(name="test", required=False, default=None, nullable=False) assert p.get_imports(prefix=prefix) == { "from io import BytesIO", - "from ...types import File", + "from ...types import File, FileJsonType", "from typing import Union", "from ...types import UNSET, Unset", } @@ -180,7 +180,7 @@ def test_get_imports(self): p = FileProperty(name="test", required=False, default=None, nullable=True) assert p.get_imports(prefix=prefix) == { "from io import BytesIO", - "from ...types import File", + "from ...types import File, FileJsonType", "from typing import Union", "from typing import Optional", "from ...types import UNSET, Unset", From f54290c7f4f3016ed0a128193107e66455eaef66 Mon Sep 17 00:00:00 2001 From: Constantinos Symeonides Date: Fri, 16 Apr 2021 17:46:04 +0100 Subject: [PATCH 03/10] fix: Non-string multipart fields must be stringified --- .../tests/upload_file_tests_upload_post.py | 5 +- .../my_test_api_client/models/__init__.py | 1 + .../body_upload_file_tests_upload_post.py | 32 +++++++++- ...load_file_tests_upload_post_some_object.py | 60 +++++++++++++++++++ end_to_end_tests/openapi.json | 24 ++++++++ .../templates/endpoint_module.py.jinja | 7 ++- 6 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py index 6b49d3284..4f48a268d 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py @@ -1,3 +1,4 @@ +import json from typing import Any, Dict, Optional, Union import httpx @@ -27,8 +28,10 @@ def _get_kwargs( for key, value in multipart_data.to_dict().items(): if is_file(value): files[key] = value - else: + elif isinstance(value, str): data[key] = value + else: + data[key] = json.dumps(value) return { "url": url, diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py index 0f7516048..7012f133e 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py @@ -9,6 +9,7 @@ from .an_int_enum import AnIntEnum from .another_all_of_sub_model import AnotherAllOfSubModel from .body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost +from .body_upload_file_tests_upload_post_some_object import BodyUploadFileTestsUploadPostSomeObject from .different_enum import DifferentEnum from .free_form_model import FreeFormModel from .http_validation_error import HTTPValidationError 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 a708a1d0b..a1f0e1eab 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 @@ -1,8 +1,9 @@ from io import BytesIO -from typing import Any, Dict, Type, TypeVar, Union +from typing import Any, Dict, List, Type, TypeVar, Union, cast import attr +from ..models.body_upload_file_tests_upload_post_some_object import BodyUploadFileTestsUploadPostSomeObject from ..types import UNSET, File, FileJsonType, Unset T = TypeVar("T", bound="BodyUploadFileTestsUploadPost") @@ -15,6 +16,9 @@ class BodyUploadFileTestsUploadPost: some_file: File some_optional_file: Union[Unset, File] = UNSET some_string: Union[Unset, str] = "some_default_string" + some_number: Union[Unset, float] = UNSET + some_array: Union[Unset, List[float]] = UNSET + some_object: Union[Unset, BodyUploadFileTestsUploadPostSomeObject] = UNSET def to_dict(self) -> Dict[str, Any]: some_file = self.some_file.to_tuple() @@ -24,6 +28,14 @@ def to_dict(self) -> Dict[str, Any]: some_optional_file = self.some_optional_file.to_tuple() some_string = self.some_string + some_number = self.some_number + some_array: Union[Unset, List[float]] = UNSET + if not isinstance(self.some_array, Unset): + some_array = self.some_array + + some_object: Union[Unset, Dict[str, Any]] = UNSET + if not isinstance(self.some_object, Unset): + some_object = self.some_object.to_dict() field_dict: Dict[str, Any] = {} field_dict.update( @@ -35,6 +47,12 @@ def to_dict(self) -> Dict[str, Any]: field_dict["some_optional_file"] = some_optional_file if some_string is not UNSET: field_dict["some_string"] = some_string + if some_number is not UNSET: + field_dict["some_number"] = some_number + if some_array is not UNSET: + field_dict["some_array"] = some_array + if some_object is not UNSET: + field_dict["some_object"] = some_object return field_dict @@ -50,10 +68,22 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: some_string = d.pop("some_string", UNSET) + some_number = d.pop("some_number", UNSET) + + some_array = cast(List[float], d.pop("some_array", UNSET)) + + some_object: Union[Unset, BodyUploadFileTestsUploadPostSomeObject] = UNSET + _some_object = d.pop("some_object", UNSET) + if not isinstance(_some_object, Unset): + some_object = BodyUploadFileTestsUploadPostSomeObject.from_dict(_some_object) + body_upload_file_tests_upload_post = cls( some_file=some_file, some_optional_file=some_optional_file, some_string=some_string, + some_number=some_number, + some_array=some_array, + some_object=some_object, ) return body_upload_file_tests_upload_post diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py new file mode 100644 index 000000000..78d6c12df --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py @@ -0,0 +1,60 @@ +from typing import Any, Dict, List, Type, TypeVar + +import attr + +T = TypeVar("T", bound="BodyUploadFileTestsUploadPostSomeObject") + + +@attr.s(auto_attribs=True) +class BodyUploadFileTestsUploadPostSomeObject: + """ """ + + num: float + text: str + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + num = self.num + text = self.text + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "num": num, + "text": text, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + num = d.pop("num") + + text = d.pop("text") + + body_upload_file_tests_upload_post_some_object = cls( + num=num, + text=text, + ) + + body_upload_file_tests_upload_post_some_object.additional_properties = d + return body_upload_file_tests_upload_post_some_object + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index b3cfcd528..f90d8476a 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -1072,6 +1072,30 @@ "title": "Some String", "type": "string", "default": "some_default_string" + }, + "some_number": { + "title": "Some Number", + "type": "number" + }, + "some_array": { + "title": "Some Array", + "type": "array", + "items": { + "type": "number" + } + }, + "some_object": { + "title": "Some Object", + "type": "object", + "required": ["num", "text"], + "properties": { + "num": { + "type": "number" + }, + "text": { + "type": "string" + } + } } }, "additionalProperties": false diff --git a/openapi_python_client/templates/endpoint_module.py.jinja b/openapi_python_client/templates/endpoint_module.py.jinja index 9702dfc99..0e7e15bbe 100644 --- a/openapi_python_client/templates/endpoint_module.py.jinja +++ b/openapi_python_client/templates/endpoint_module.py.jinja @@ -1,6 +1,9 @@ from typing import Any, Dict, List, Optional, Union, cast import httpx +{% if endpoint.multipart_body_class %} +import json +{% endif %} from ...client import AuthenticatedClient, Client from ...types import Response, UNSET{% if endpoint.multipart_body_class %}, is_file {% endif %} @@ -41,8 +44,10 @@ def _get_kwargs( for key, value in multipart_data.to_dict().items(): if is_file(value): files[key] = value - else: + elif isinstance(value, str): data[key] = value + else: + data[key] = json.dumps(value) {% endif %} return { From bfc88d2e86c562d1a27465d16ad0997bd35ebbe2 Mon Sep 17 00:00:00 2001 From: Constantinos Symeonides Date: Mon, 17 May 2021 11:58:12 +0100 Subject: [PATCH 04/10] chore: Reformat --- .../models/body_upload_file_tests_upload_post.py | 12 ++++++++---- ...body_upload_file_tests_upload_post_some_object.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) 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 a1f0e1eab..7e37410b1 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 @@ -61,9 +61,11 @@ 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_optional_file: Union[Unset, File] = UNSET _some_optional_file = d.pop("some_optional_file", UNSET) - if not isinstance(_some_optional_file, Unset): + some_optional_file: Union[Unset, File] + if isinstance(_some_optional_file, Unset): + some_optional_file = UNSET + else: some_optional_file = File(payload=BytesIO(_some_optional_file)) some_string = d.pop("some_string", UNSET) @@ -72,9 +74,11 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: some_array = cast(List[float], d.pop("some_array", UNSET)) - some_object: Union[Unset, BodyUploadFileTestsUploadPostSomeObject] = UNSET _some_object = d.pop("some_object", UNSET) - if not isinstance(_some_object, Unset): + some_object: Union[Unset, BodyUploadFileTestsUploadPostSomeObject] + if isinstance(_some_object, Unset): + some_object = UNSET + else: some_object = BodyUploadFileTestsUploadPostSomeObject.from_dict(_some_object) body_upload_file_tests_upload_post = cls( diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py index 78d6c12df..85eaba04e 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_object.py @@ -7,7 +7,7 @@ @attr.s(auto_attribs=True) class BodyUploadFileTestsUploadPostSomeObject: - """ """ + """ """ num: float text: str From fd7f80a6d92d480162a6ff9195c86563538676f6 Mon Sep 17 00:00:00 2001 From: Constantinos Symeonides Date: Mon, 17 May 2021 18:35:00 +0100 Subject: [PATCH 05/10] fix: Put non-file fields as tuples in the files dict --- .../api/tests/upload_file_tests_upload_post.py | 8 ++------ openapi_python_client/templates/endpoint_module.py.jinja | 8 ++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py index 4f48a268d..4d240da65 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py @@ -24,14 +24,11 @@ def _get_kwargs( headers["keep-alive"] = keep_alive files = {} - data = {} for key, value in multipart_data.to_dict().items(): - if is_file(value): + if is_file(value) or isinstance(value, str): files[key] = value - elif isinstance(value, str): - data[key] = value else: - data[key] = json.dumps(value) + files[key] = (None, json.dumps(value), "application/json") return { "url": url, @@ -39,7 +36,6 @@ def _get_kwargs( "cookies": cookies, "timeout": client.get_timeout(), "files": files, - "data": data, } diff --git a/openapi_python_client/templates/endpoint_module.py.jinja b/openapi_python_client/templates/endpoint_module.py.jinja index 0e7e15bbe..81fc2e3e9 100644 --- a/openapi_python_client/templates/endpoint_module.py.jinja +++ b/openapi_python_client/templates/endpoint_module.py.jinja @@ -40,14 +40,11 @@ def _get_kwargs( {% if endpoint.multipart_body_class %} files = {} - data = {} for key, value in multipart_data.to_dict().items(): - if is_file(value): + if is_file(value) or isinstance(value, str): files[key] = value - elif isinstance(value, str): - data[key] = value else: - data[key] = json.dumps(value) + files[key] = (None, json.dumps(value), "application/json") {% endif %} return { @@ -59,7 +56,6 @@ def _get_kwargs( "data": form_data.to_dict(), {% elif endpoint.multipart_body_class %} "files": files, - "data": data, {% elif endpoint.json_body %} "json": {{ "json_" + endpoint.json_body.python_name }}, {% endif %} From 0a220bbc2462c8d724d3a9667c1b7d01aa8e8496 Mon Sep 17 00:00:00 2001 From: Constantinos Symeonides Date: Mon, 17 May 2021 20:42:46 +0100 Subject: [PATCH 06/10] refactor: Avoid runtime type checks --- .../tests/upload_file_tests_upload_post.py | 30 ++-- .../my_test_api_client/models/__init__.py | 2 + .../body_upload_file_tests_upload_post.py | 99 +++++++++++-- ..._tests_upload_post_some_nullable_object.py | 54 +++++++ ..._tests_upload_post_some_optional_object.py | 54 +++++++ .../golden-record/my_test_api_client/types.py | 7 +- end_to_end_tests/openapi.json | 24 ++- openapi_python_client/parser/openapi.py | 48 ++++-- .../parser/properties/model_property.py | 1 + .../templates/endpoint_macros.py.jinja | 19 ++- .../templates/endpoint_module.py.jinja | 20 +-- .../templates/model.py.jinja | 79 +++++----- .../property_templates/date_property.py.jinja | 2 +- .../datetime_property.py.jinja | 2 +- .../property_templates/enum_property.py.jinja | 2 +- .../property_templates/file_property.py.jinja | 2 +- .../property_templates/list_property.py.jinja | 15 +- .../model_property.py.jinja | 18 ++- .../property_templates/none_property.py.jinja | 2 +- .../union_property.py.jinja | 4 +- .../templates/types.py.jinja | 5 - tests/test_parser/test_openapi.py | 140 ++++++++++++++---- 22 files changed, 472 insertions(+), 157 deletions(-) create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_nullable_object.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_optional_object.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py index 4d240da65..e0c29fd74 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py @@ -1,4 +1,3 @@ -import json from typing import Any, Dict, Optional, Union import httpx @@ -6,13 +5,13 @@ from ...client import Client from ...models.body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost from ...models.http_validation_error import HTTPValidationError -from ...types import UNSET, Response, Unset, is_file +from ...types import UNSET, Response, Unset def _get_kwargs( *, client: Client, - multipart_data: BodyUploadFileTestsUploadPost, + multipart_body: BodyUploadFileTestsUploadPost, keep_alive: Union[Unset, bool] = UNSET, ) -> Dict[str, Any]: url = "{}/tests/upload".format(client.base_url) @@ -23,19 +22,14 @@ def _get_kwargs( if keep_alive is not UNSET: headers["keep-alive"] = keep_alive - files = {} - for key, value in multipart_data.to_dict().items(): - if is_file(value) or isinstance(value, str): - files[key] = value - else: - files[key] = (None, json.dumps(value), "application/json") + multipart_multipart_body = multipart_body.to_multipart() return { "url": url, "headers": headers, "cookies": cookies, "timeout": client.get_timeout(), - "files": files, + "files": multipart_multipart_body, } @@ -63,12 +57,12 @@ def _build_response(*, response: httpx.Response) -> Response[Union[HTTPValidatio def sync_detailed( *, client: Client, - multipart_data: BodyUploadFileTestsUploadPost, + multipart_body: BodyUploadFileTestsUploadPost, keep_alive: Union[Unset, bool] = UNSET, ) -> Response[Union[HTTPValidationError, None]]: kwargs = _get_kwargs( client=client, - multipart_data=multipart_data, + multipart_body=multipart_body, keep_alive=keep_alive, ) @@ -82,14 +76,14 @@ def sync_detailed( def sync( *, client: Client, - multipart_data: BodyUploadFileTestsUploadPost, + multipart_body: BodyUploadFileTestsUploadPost, keep_alive: Union[Unset, bool] = UNSET, ) -> Optional[Union[HTTPValidationError, None]]: """Upload a file""" return sync_detailed( client=client, - multipart_data=multipart_data, + multipart_body=multipart_body, keep_alive=keep_alive, ).parsed @@ -97,12 +91,12 @@ def sync( async def asyncio_detailed( *, client: Client, - multipart_data: BodyUploadFileTestsUploadPost, + multipart_body: BodyUploadFileTestsUploadPost, keep_alive: Union[Unset, bool] = UNSET, ) -> Response[Union[HTTPValidationError, None]]: kwargs = _get_kwargs( client=client, - multipart_data=multipart_data, + multipart_body=multipart_body, keep_alive=keep_alive, ) @@ -115,7 +109,7 @@ async def asyncio_detailed( async def asyncio( *, client: Client, - multipart_data: BodyUploadFileTestsUploadPost, + multipart_body: BodyUploadFileTestsUploadPost, keep_alive: Union[Unset, bool] = UNSET, ) -> Optional[Union[HTTPValidationError, None]]: """Upload a file""" @@ -123,7 +117,7 @@ async def asyncio( return ( await asyncio_detailed( client=client, - multipart_data=multipart_data, + multipart_body=multipart_body, keep_alive=keep_alive, ) ).parsed diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py index 7012f133e..575a68e67 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py @@ -9,7 +9,9 @@ from .an_int_enum import AnIntEnum from .another_all_of_sub_model import AnotherAllOfSubModel from .body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost +from .body_upload_file_tests_upload_post_some_nullable_object import BodyUploadFileTestsUploadPostSomeNullableObject from .body_upload_file_tests_upload_post_some_object import BodyUploadFileTestsUploadPostSomeObject +from .body_upload_file_tests_upload_post_some_optional_object import BodyUploadFileTestsUploadPostSomeOptionalObject from .different_enum import DifferentEnum from .free_form_model import FreeFormModel from .http_validation_error import HTTPValidationError 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 7e37410b1..52a490529 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 @@ -1,9 +1,16 @@ +import json from io import BytesIO -from typing import Any, Dict, List, Type, TypeVar, Union, cast +from typing import Any, Dict, List, Optional, Type, TypeVar, Union, cast import attr +from ..models.body_upload_file_tests_upload_post_some_nullable_object import ( + BodyUploadFileTestsUploadPostSomeNullableObject, +) from ..models.body_upload_file_tests_upload_post_some_object import BodyUploadFileTestsUploadPostSomeObject +from ..models.body_upload_file_tests_upload_post_some_optional_object import ( + BodyUploadFileTestsUploadPostSomeOptionalObject, +) from ..types import UNSET, File, FileJsonType, Unset T = TypeVar("T", bound="BodyUploadFileTestsUploadPost") @@ -14,15 +21,19 @@ class BodyUploadFileTestsUploadPost: """ """ some_file: File + some_object: BodyUploadFileTestsUploadPostSomeObject + some_nullable_object: Optional[BodyUploadFileTestsUploadPostSomeNullableObject] some_optional_file: Union[Unset, File] = UNSET some_string: Union[Unset, str] = "some_default_string" some_number: Union[Unset, float] = UNSET some_array: Union[Unset, List[float]] = UNSET - some_object: Union[Unset, BodyUploadFileTestsUploadPostSomeObject] = UNSET + some_optional_object: Union[Unset, BodyUploadFileTestsUploadPostSomeOptionalObject] = UNSET def to_dict(self) -> Dict[str, Any]: some_file = self.some_file.to_tuple() + some_object = self.some_object.to_dict() + some_optional_file: Union[Unset, FileJsonType] = UNSET if not isinstance(self.some_optional_file, Unset): some_optional_file = self.some_optional_file.to_tuple() @@ -33,14 +44,65 @@ def to_dict(self) -> Dict[str, Any]: if not isinstance(self.some_array, Unset): some_array = self.some_array - some_object: Union[Unset, Dict[str, Any]] = UNSET - if not isinstance(self.some_object, Unset): - some_object = self.some_object.to_dict() + some_optional_object: Union[Unset, Dict[str, Any]] = UNSET + if not isinstance(self.some_optional_object, Unset): + some_optional_object = self.some_optional_object.to_dict() + + some_nullable_object = self.some_nullable_object.to_dict() if self.some_nullable_object else None + + field_dict: Dict[str, Any] = {} + field_dict.update( + { + "some_file": some_file, + "some_object": some_object, + "some_nullable_object": some_nullable_object, + } + ) + if some_optional_file is not UNSET: + field_dict["some_optional_file"] = some_optional_file + if some_string is not UNSET: + field_dict["some_string"] = some_string + if some_number is not UNSET: + field_dict["some_number"] = some_number + if some_array is not UNSET: + field_dict["some_array"] = some_array + if some_optional_object is not UNSET: + field_dict["some_optional_object"] = some_optional_object + + return field_dict + + def to_multipart(self) -> Dict[str, Any]: + some_file = self.some_file.to_tuple() + + some_object = (None, json.dumps(self.some_object.to_dict()), "application/json") + + some_optional_file = UNSET + if not isinstance(self.some_optional_file, Unset): + some_optional_file = self.some_optional_file.to_tuple() + + some_string = self.some_string + some_number = self.some_number + some_array = UNSET + if not isinstance(self.some_array, Unset): + some_array = self.some_array + some_array = (None, json.dumps(some_array), "application/json") + + some_optional_object = UNSET + if not isinstance(self.some_optional_object, Unset): + some_optional_object = (None, json.dumps(self.some_optional_object.to_dict()), "application/json") + + some_nullable_object = ( + (None, json.dumps(self.some_nullable_object.to_dict()), "application/json") + if self.some_nullable_object + else None + ) field_dict: Dict[str, Any] = {} field_dict.update( { "some_file": some_file, + "some_object": some_object, + "some_nullable_object": some_nullable_object, } ) if some_optional_file is not UNSET: @@ -51,8 +113,8 @@ def to_dict(self) -> Dict[str, Any]: field_dict["some_number"] = some_number if some_array is not UNSET: field_dict["some_array"] = some_array - if some_object is not UNSET: - field_dict["some_object"] = some_object + if some_optional_object is not UNSET: + field_dict["some_optional_object"] = some_optional_object return field_dict @@ -61,6 +123,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_object = BodyUploadFileTestsUploadPostSomeObject.from_dict(d.pop("some_object")) + _some_optional_file = d.pop("some_optional_file", UNSET) some_optional_file: Union[Unset, File] if isinstance(_some_optional_file, Unset): @@ -74,20 +138,29 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: some_array = cast(List[float], d.pop("some_array", UNSET)) - _some_object = d.pop("some_object", UNSET) - some_object: Union[Unset, BodyUploadFileTestsUploadPostSomeObject] - if isinstance(_some_object, Unset): - some_object = UNSET + _some_optional_object = d.pop("some_optional_object", UNSET) + some_optional_object: Union[Unset, BodyUploadFileTestsUploadPostSomeOptionalObject] + if isinstance(_some_optional_object, Unset): + some_optional_object = UNSET else: - some_object = BodyUploadFileTestsUploadPostSomeObject.from_dict(_some_object) + some_optional_object = BodyUploadFileTestsUploadPostSomeOptionalObject.from_dict(_some_optional_object) + + _some_nullable_object = d.pop("some_nullable_object") + some_nullable_object: Optional[BodyUploadFileTestsUploadPostSomeNullableObject] + if _some_nullable_object is None: + some_nullable_object = None + else: + some_nullable_object = BodyUploadFileTestsUploadPostSomeNullableObject.from_dict(_some_nullable_object) body_upload_file_tests_upload_post = cls( some_file=some_file, + some_object=some_object, some_optional_file=some_optional_file, some_string=some_string, some_number=some_number, some_array=some_array, - some_object=some_object, + some_optional_object=some_optional_object, + some_nullable_object=some_nullable_object, ) return body_upload_file_tests_upload_post diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_nullable_object.py b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_nullable_object.py new file mode 100644 index 000000000..f97e865aa --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_nullable_object.py @@ -0,0 +1,54 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="BodyUploadFileTestsUploadPostSomeNullableObject") + + +@attr.s(auto_attribs=True) +class BodyUploadFileTestsUploadPostSomeNullableObject: + """ """ + + bar: Union[Unset, str] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + bar = self.bar + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if bar is not UNSET: + field_dict["bar"] = bar + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + bar = d.pop("bar", UNSET) + + body_upload_file_tests_upload_post_some_nullable_object = cls( + bar=bar, + ) + + body_upload_file_tests_upload_post_some_nullable_object.additional_properties = d + return body_upload_file_tests_upload_post_some_nullable_object + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_optional_object.py b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_optional_object.py new file mode 100644 index 000000000..f983f83f4 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_some_optional_object.py @@ -0,0 +1,54 @@ +from typing import Any, Dict, List, Type, TypeVar + +import attr + +T = TypeVar("T", bound="BodyUploadFileTestsUploadPostSomeOptionalObject") + + +@attr.s(auto_attribs=True) +class BodyUploadFileTestsUploadPostSomeOptionalObject: + """ """ + + foo: str + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + foo = self.foo + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "foo": foo, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + foo = d.pop("foo") + + body_upload_file_tests_upload_post_some_optional_object = cls( + foo=foo, + ) + + body_upload_file_tests_upload_post_some_optional_object.additional_properties = d + return body_upload_file_tests_upload_post_some_optional_object + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/types.py b/end_to_end_tests/golden-record/my_test_api_client/types.py index 668dfec37..c47d4f1e3 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/types.py +++ b/end_to_end_tests/golden-record/my_test_api_client/types.py @@ -1,6 +1,5 @@ """ Contains some shared types for properties """ -from io import IOBase -from typing import Any, BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union +from typing import BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union import attr @@ -15,10 +14,6 @@ def __bool__(self) -> bool: FileJsonType = Tuple[Optional[str], Union[BinaryIO, TextIO], Optional[str]] -def is_file(value: Any) -> bool: - return isinstance(value, tuple) and len(value) == 3 and isinstance(value[1], IOBase) - - @attr.s(auto_attribs=True) class File: """Contains information for file uploads""" diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index f90d8476a..44d6eaa67 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -1053,9 +1053,7 @@ }, "Body_upload_file_tests_upload_post": { "title": "Body_upload_file_tests_upload_post", - "required": [ - "some_file" - ], + "required": ["some_file", "some_object", "some_nullable_object"], "type": "object", "properties": { "some_file": { @@ -1096,6 +1094,26 @@ "type": "string" } } + }, + "some_optional_object": { + "title": "Some Optional Object", + "type": "object", + "required": ["foo"], + "properties": { + "foo": { + "type": "string" + } + } + }, + "some_nullable_object": { + "title": "Some Nullable Object", + "type": "object", + "nullable": true, + "properties": { + "bar": { + "type": "string" + } + } } }, "additionalProperties": false diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index f0b9774ce..8ea8d4afd 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -3,6 +3,7 @@ from dataclasses import dataclass, field from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union +import attr from pydantic import ValidationError from .. import schema as oai @@ -94,7 +95,7 @@ class Endpoint: responses: List[Response] = field(default_factory=list) form_body_class: Optional[Class] = None json_body: Optional[Property] = None - multipart_body_class: Optional[Class] = None + multipart_body: Optional[Property] = None errors: List[ParseError] = field(default_factory=list) @staticmethod @@ -107,13 +108,26 @@ def parse_request_form_body(*, body: oai.RequestBody, config: Config) -> Optiona return None @staticmethod - def parse_multipart_body(*, body: oai.RequestBody, config: Config) -> Optional[Class]: - """Return form_body_reference""" + def parse_multipart_body( + *, body: oai.RequestBody, schemas: Schemas, parent_name: str, config: Config + ) -> Tuple[Union[Property, PropertyError, None], Schemas]: + """Return multipart_body""" body_content = body.content - json_body = body_content.get("multipart/form-data") - if json_body is not None and isinstance(json_body.media_type_schema, oai.Reference): - return Class.from_string(string=json_body.media_type_schema.ref, config=config) - return None + multipart_body = body_content.get("multipart/form-data") + if multipart_body is not None and multipart_body.media_type_schema is not None: + prop, schemas = property_from_data( + name="multipart_body", + required=True, + data=multipart_body.media_type_schema, + schemas=schemas, + parent_name=parent_name, + config=config, + ) + if isinstance(prop, ModelProperty): + prop = attr.evolve(prop, is_multipart_body=True) + schemas = attr.evolve(schemas, classes_by_name={**schemas.classes_by_name, prop.class_info.name: prop}) + return prop, schemas + return None, schemas @staticmethod def parse_request_json_body( @@ -153,19 +167,31 @@ def _add_body( if isinstance(json_body, ParseError): return ( ParseError( - header=f"Cannot parse body of endpoint {endpoint.name}", + header=f"Cannot parse JSON body of endpoint {endpoint.name}", detail=json_body.detail, data=json_body.data, ), schemas, ) - endpoint.multipart_body_class = Endpoint.parse_multipart_body(body=data.requestBody, config=config) + multipart_body, schemas = Endpoint.parse_multipart_body( + body=data.requestBody, schemas=schemas, parent_name=endpoint.name, config=config + ) + if isinstance(multipart_body, ParseError): + return ( + ParseError( + header=f"Cannot parse multipart body of endpoint {endpoint.name}", + detail=multipart_body.detail, + data=multipart_body.data, + ), + schemas, + ) if endpoint.form_body_class: endpoint.relative_imports.add(import_string_from_class(endpoint.form_body_class, prefix="...models")) - if endpoint.multipart_body_class: - endpoint.relative_imports.add(import_string_from_class(endpoint.multipart_body_class, prefix="...models")) + if multipart_body is not None: + endpoint.multipart_body = multipart_body + endpoint.relative_imports.update(endpoint.multipart_body.get_imports(prefix="...")) if json_body is not None: endpoint.json_body = json_body endpoint.relative_imports.update(endpoint.json_body.get_imports(prefix="...")) diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index a40460886..79ac48764 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -25,6 +25,7 @@ class ModelProperty(Property): template: ClassVar[str] = "model_property.py.jinja" json_is_dict: ClassVar[bool] = True + is_multipart_body: bool = False def get_base_type_string(self, json: bool = False) -> str: return self.class_info.name diff --git a/openapi_python_client/templates/endpoint_macros.py.jinja b/openapi_python_client/templates/endpoint_macros.py.jinja index 66d6209b3..3723a2f60 100644 --- a/openapi_python_client/templates/endpoint_macros.py.jinja +++ b/openapi_python_client/templates/endpoint_macros.py.jinja @@ -71,6 +71,17 @@ params = {k: v for k, v in params.items() if v is not UNSET and v is not None} {% endif %} {% endmacro %} +{% macro multipart_body(endpoint) %} +{% if endpoint.multipart_body %} + {% set property = endpoint.multipart_body %} + {% set destination = "multipart_" + property.python_name %} + {% if property.template %} + {% from "property_templates/" + property.template import transform_multipart %} +{{ transform_multipart(property, property.python_name, destination) }} + {% endif %} +{% endif %} +{% endmacro %} + {# The all the kwargs passed into an endpoint (and variants thereof)) #} {% macro arguments(endpoint) %} *, @@ -89,8 +100,8 @@ client: Client, form_data: {{ endpoint.form_body_class.name }}, {% endif %} {# Multipart data if any #} -{% if endpoint.multipart_body_class %} -multipart_data: {{ endpoint.multipart_body_class.name }}, +{% if endpoint.multipart_body %} +multipart_body: {{ endpoint.multipart_body.get_type_string() }}, {% endif %} {# JSON body if any #} {% if endpoint.json_body %} @@ -118,8 +129,8 @@ client=client, {% if endpoint.form_body_class %} form_data=form_data, {% endif %} -{% if endpoint.multipart_body_class %} -multipart_data=multipart_data, +{% if endpoint.multipart_body %} +multipart_body=multipart_body, {% endif %} {% if endpoint.json_body %} json_body=json_body, diff --git a/openapi_python_client/templates/endpoint_module.py.jinja b/openapi_python_client/templates/endpoint_module.py.jinja index 81fc2e3e9..a55ea14cd 100644 --- a/openapi_python_client/templates/endpoint_module.py.jinja +++ b/openapi_python_client/templates/endpoint_module.py.jinja @@ -1,18 +1,15 @@ from typing import Any, Dict, List, Optional, Union, cast import httpx -{% if endpoint.multipart_body_class %} -import json -{% endif %} from ...client import AuthenticatedClient, Client -from ...types import Response, UNSET{% if endpoint.multipart_body_class %}, is_file {% endif %} +from ...types import Response, UNSET {% for relative in endpoint.relative_imports %} {{ relative }} {% endfor %} -{% from "endpoint_macros.py.jinja" import header_params, cookie_params, query_params, json_body, arguments, client, kwargs, parse_response %} +{% from "endpoint_macros.py.jinja" import header_params, cookie_params, query_params, json_body, multipart_body, arguments, client, kwargs, parse_response %} {% set return_string = endpoint.response_type() %} {% set parsed_responses = (endpoint.responses | length > 0) and return_string != "None" %} @@ -38,14 +35,7 @@ def _get_kwargs( {{ json_body(endpoint) | indent(4) }} - {% if endpoint.multipart_body_class %} - files = {} - for key, value in multipart_data.to_dict().items(): - if is_file(value) or isinstance(value, str): - files[key] = value - else: - files[key] = (None, json.dumps(value), "application/json") - {% endif %} + {{ multipart_body(endpoint) | indent(4) }} return { "url": url, @@ -54,8 +44,8 @@ def _get_kwargs( "timeout": client.get_timeout(), {% if endpoint.form_body_class %} "data": form_data.to_dict(), - {% elif endpoint.multipart_body_class %} - "files": files, + {% elif endpoint.multipart_body %} + "files": {{ "multipart_" + endpoint.multipart_body.python_name }}, {% elif endpoint.json_body %} "json": {{ "json_" + endpoint.json_body.python_name }}, {% endif %} diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index 0cc98b105..a2deadf81 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -6,6 +6,9 @@ from typing import List {% endif %} import attr +{% if model.is_multipart_body %} +import json +{% endif %} from ..types import UNSET, Unset @@ -40,42 +43,50 @@ class {{ class_name }}: additional_properties: Dict[str, {{ additional_property_type }}] = attr.ib(init=False, factory=dict) {% endif %} +{% macro _to_dict(multipart=False) %} +{% for property in model.required_properties + model.optional_properties %} +{% if property.template %} +{% from "property_templates/" + property.template import transform %} +{{ transform(property, "self." + property.python_name, property.python_name, declare_type=not multipart, stringify=multipart) }} +{% else %} +{{ property.python_name }} = self.{{ property.python_name }} +{% endif %} +{% endfor %} + +field_dict: Dict[str, Any] = {} +{% if model.additional_properties %} +{% if model.additional_properties.template %} +{% from "property_templates/" + model.additional_properties.template import transform %} +for prop_name, prop in self.additional_properties.items(): + {{ transform(model.additional_properties, "prop", "field_dict[prop_name]", declare_type=not multipart, stringify=multipart) | indent(4) }} +{% else %} +field_dict.update(self.additional_properties) +{% endif %} +{% endif %} +field_dict.update({ + {% for property in model.required_properties + model.optional_properties %} + {% if property.required %} + "{{ property.name }}": {{ property.python_name }}, + {% endif %} + {% endfor %} +}) +{% for property in model.optional_properties %} +{% if not property.required %} +if {{ property.python_name }} is not UNSET: + field_dict["{{ property.name }}"] = {{ property.python_name }} +{% endif %} +{% endfor %} + +return field_dict +{% endmacro %} def to_dict(self) -> Dict[str, Any]: - {% for property in model.required_properties + model.optional_properties %} - {% if property.template %} - {% from "property_templates/" + property.template import transform %} - {{ transform(property, "self." + property.python_name, property.python_name) | indent(8) }} - {% else %} - {{ property.python_name }} = self.{{ property.python_name }} - {% endif %} - {% endfor %} - - field_dict: Dict[str, Any] = {} - {% if model.additional_properties %} - {% if model.additional_properties.template %} - {% from "property_templates/" + model.additional_properties.template import transform %} - for prop_name, prop in self.additional_properties.items(): - {{ transform(model.additional_properties, "prop", "field_dict[prop_name]") | indent(12) }} - {% else %} - field_dict.update(self.additional_properties) - {% endif %} - {% endif %} - field_dict.update({ - {% for property in model.required_properties + model.optional_properties %} - {% if property.required %} - "{{ property.name }}": {{ property.python_name }}, - {% endif %} - {% endfor %} - }) - {% for property in model.optional_properties %} - {% if not property.required %} - if {{ property.python_name }} is not UNSET: - field_dict["{{ property.name }}"] = {{ property.python_name }} - {% endif %} - {% endfor %} - - return field_dict + {{ _to_dict() | indent(8) }} + +{% if model.is_multipart_body %} + def to_multipart(self) -> Dict[str, Any]: + {{ _to_dict(multipart=True) | indent(8) }} +{% endif %} @classmethod def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: 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 65672d2e7..7c4cebfbd 100644 --- a/openapi_python_client/templates/property_templates/date_property.py.jinja +++ b/openapi_python_client/templates/property_templates/date_property.py.jinja @@ -10,7 +10,7 @@ isoparse({{ source }}).date() {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, str){% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, stringify=False) %} {% if property.required %} {{ destination }} = {{ source }}.isoformat() {% if property.nullable %}if {{ source }} else None {%endif%} {% else %} 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 de1e8427f..0984773e0 100644 --- a/openapi_python_client/templates/property_templates/datetime_property.py.jinja +++ b/openapi_python_client/templates/property_templates/datetime_property.py.jinja @@ -10,7 +10,7 @@ isoparse({{ source }}) {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, str){% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, stringify=False) %} {% if property.required %} {% if property.nullable %} {{ destination }} = {{ source }}.isoformat() if {{ source }} else None 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 9dd051b38..46e1ebe35 100644 --- a/openapi_python_client/templates/property_templates/enum_property.py.jinja +++ b/openapi_python_client/templates/property_templates/enum_property.py.jinja @@ -10,7 +10,7 @@ {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, {{ property.value_type.__name__ }}){% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, stringify=False) %} {% if property.required %} {% if property.nullable %} {{ destination }} = {{ source }}.value if {{ source }} else None 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 f8fd0c193..e63cac53d 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) %} +{% macro transform(property, source, destination, declare_type=True, stringify=False) %} {% if property.required %} {% if property.nullable %} {{ destination }} = {{ source }}.to_tuple() if {{ source }} else None 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 c6ef85254..ba9926868 100644 --- a/openapi_python_client/templates/property_templates/list_property.py.jinja +++ b/openapi_python_client/templates/property_templates/list_property.py.jinja @@ -17,7 +17,7 @@ for {{ inner_source }} in (_{{ property.python_name }} or []): {% endif %} {% endmacro %} -{% macro _transform(property, source, destination) %} +{% macro _transform(property, source, destination, stringify) %} {% set inner_property = property.inner_property %} {% if inner_property.template %} {% set inner_source = inner_property.python_name + "_data" %} @@ -29,20 +29,23 @@ for {{ inner_source }} in {{ source }}: {% else %} {{ destination }} = {{ source }} {% endif %} +{% if stringify %} +{{ destination }} = (None, json.dumps({{ destination }}), 'application/json') +{% endif %} {% endmacro %} {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, list){% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, stringify=False) %} {% set inner_property = property.inner_property %} {% if property.required %} {% if property.nullable %} if {{ source }} is None: {{ destination }} = None else: - {{ _transform(property, source, destination) | indent(4) }} + {{ _transform(property, source, destination, stringify) | indent(4) }} {% else %} -{{ _transform(property, source, destination) }} +{{ _transform(property, source, destination, stringify) }} {% endif %} {% else %} {{ destination }}{% if declare_type %}: {{ property.get_type_string(json=True) }}{% endif %} = UNSET @@ -51,9 +54,9 @@ if not isinstance({{ source }}, Unset): if {{ source }} is None: {{ destination }} = None else: - {{ _transform(property, source, destination) | indent(8)}} + {{ _transform(property, source, destination, stringify) | indent(8)}} {% else %} - {{ _transform(property, source, destination) | indent(4)}} + {{ _transform(property, source, destination, stringify) | indent(4)}} {% endif %} {% endif %} 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 2772918cf..660317e41 100644 --- a/openapi_python_client/templates/property_templates/model_property.py.jinja +++ b/openapi_python_client/templates/property_templates/model_property.py.jinja @@ -10,20 +10,28 @@ {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, dict){% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, stringify=False, transform_method="to_dict") %} +{% set transformed = source + "." + transform_method + "()" %} +{% if stringify %} +{% set transformed = "(None, json.dumps(" + transformed + "), 'application/json')" %} +{% endif %} {% if property.required %} {% if property.nullable %} -{{ destination }} = {{ source }}.to_dict() if {{ source }} else None +{{ destination }} = {{ transformed }} if {{ source }} else None {% else %} -{{ destination }} = {{ source }}.to_dict() +{{ destination }} = {{ transformed }} {% endif %} {% else %} {{ destination }}{% if declare_type %}: {{ property.get_type_string(json=True) }}{% endif %} = UNSET if not isinstance({{ source }}, Unset): {% if property.nullable %} - {{ destination }} = {{ source }}.to_dict() if {{ source }} else None + {{ destination }} = {{ transformed }} if {{ source }} else None {% else %} - {{ destination }} = {{ source }}.to_dict() + {{ destination }} = {{ transformed }} {% endif %} {% endif %} {% endmacro %} + +{% macro transform_multipart(property, source, destination) %} +{{ transform(property, source, destination, transform_method="to_multipart") }} +{% endmacro %} diff --git a/openapi_python_client/templates/property_templates/none_property.py.jinja b/openapi_python_client/templates/property_templates/none_property.py.jinja index adc6b1524..864802c28 100644 --- a/openapi_python_client/templates/property_templates/none_property.py.jinja +++ b/openapi_python_client/templates/property_templates/none_property.py.jinja @@ -4,6 +4,6 @@ {% macro check_type_for_construct(property, source) %}{{ source }} is None{% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, stringify=False) %} {{ destination }} = None {% 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 87ea9820f..ce988a913 100644 --- a/openapi_python_client/templates/property_templates/union_property.py.jinja +++ b/openapi_python_client/templates/property_templates/union_property.py.jinja @@ -37,7 +37,7 @@ def _parse_{{ property.python_name }}(data: object) -> {{ property.get_type_stri {# For now we assume there will be no unions of unions #} {% macro check_type_for_construct(property, source) %}True{% endmacro %} -{% macro transform(property, source, destination, declare_type=True) %} +{% macro transform(property, source, destination, declare_type=True, stringify=False) %} {% if not property.required or property.nullable %} {{ destination }}{% if declare_type %}: {{ property.get_type_string(json=True) }}{% endif %} @@ -63,7 +63,7 @@ elif isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): else: {% endif %} {% from "property_templates/" + inner_property.template import transform %} - {{ transform(inner_property, source, destination, declare_type=False) | indent(4) }} + {{ transform(inner_property, source, destination, declare_type=False, stringify=stringify) | indent(4) }} {% endfor %} {% if property.has_properties_without_templates and (property.inner_properties_with_template() | any or not property.required)%} else: diff --git a/openapi_python_client/templates/types.py.jinja b/openapi_python_client/templates/types.py.jinja index 54293eaa0..c167cb7cc 100644 --- a/openapi_python_client/templates/types.py.jinja +++ b/openapi_python_client/templates/types.py.jinja @@ -1,5 +1,4 @@ """ Contains some shared types for properties """ -from io import IOBase from typing import Any, BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union import attr @@ -16,10 +15,6 @@ UNSET: Unset = Unset() FileJsonType = Tuple[Optional[str], Union[BinaryIO, TextIO], Optional[str]] -def is_file(value: Any) -> bool: - return isinstance(value, tuple) and len(value) == 3 and isinstance(value[1], IOBase) - - @attr.s(auto_attribs=True) class File: """ Contains information for file uploads """ diff --git a/tests/test_parser/test_openapi.py b/tests/test_parser/test_openapi.py index a220d243c..a7bbdec28 100644 --- a/tests/test_parser/test_openapi.py +++ b/tests/test_parser/test_openapi.py @@ -167,29 +167,79 @@ def test_parse_request_form_body_no_data(self): assert result is None - def test_parse_multipart_body(self, mocker): - ref = mocker.MagicMock() + def test_parse_multipart_body(self, mocker, model_property_factory): + from openapi_python_client.parser.openapi import Endpoint, Schemas + from openapi_python_client.parser.properties import Class + + class_info = Class(name="class_name", module_name="module_name") + prop_before = model_property_factory(class_info=class_info, is_multipart_body=False) + + schema = mocker.MagicMock() body = oai.RequestBody.construct( - content={"multipart/form-data": oai.MediaType.construct(media_type_schema=oai.Reference.construct(ref=ref))} + content={"multipart/form-data": oai.MediaType.construct(media_type_schema=schema)} ) - from_string = mocker.patch(f"{MODULE_NAME}.Class.from_string") + schemas_before = Schemas() config = MagicMock() + property_from_data = mocker.patch( + f"{MODULE_NAME}.property_from_data", return_value=(prop_before, schemas_before) + ) - from openapi_python_client.parser.openapi import Endpoint + result = Endpoint.parse_multipart_body(body=body, schemas=schemas_before, parent_name="parent", config=config) - result = Endpoint.parse_multipart_body(body=body, config=config) + property_from_data.assert_called_once_with( + name="multipart_body", + required=True, + data=schema, + schemas=schemas_before, + parent_name="parent", + config=config, + ) + prop_after = model_property_factory(class_info=class_info, is_multipart_body=True) + schemas_after = Schemas(classes_by_name={class_info.name: prop_after}) + assert result == (prop_after, schemas_after) - from_string.assert_called_once_with(string=ref, config=config) - assert result == from_string.return_value + def test_parse_multipart_body_existing_schema(self, mocker, model_property_factory): + from openapi_python_client.parser.openapi import Endpoint, Schemas + from openapi_python_client.parser.properties import Class + + class_info = Class(name="class_name", module_name="module_name") + prop_before = model_property_factory(class_info=class_info, is_multipart_body=False) + schemas_before = Schemas(classes_by_name={class_info.name: prop_before}) + + schema = mocker.MagicMock() + body = oai.RequestBody.construct( + content={"multipart/form-data": oai.MediaType.construct(media_type_schema=schema)} + ) + config = MagicMock() + property_from_data = mocker.patch( + f"{MODULE_NAME}.property_from_data", return_value=(prop_before, schemas_before) + ) + + result = Endpoint.parse_multipart_body(body=body, schemas=schemas_before, parent_name="parent", config=config) + + property_from_data.assert_called_once_with( + name="multipart_body", + required=True, + data=schema, + schemas=schemas_before, + parent_name="parent", + config=config, + ) + prop_after = model_property_factory(class_info=class_info, is_multipart_body=True) + schemas_after = Schemas(classes_by_name={class_info.name: prop_after}) + assert result == (prop_after, schemas_after) def test_parse_multipart_body_no_data(self): - body = oai.RequestBody.construct(content={}) + from openapi_python_client.parser.openapi import Endpoint, Schemas - from openapi_python_client.parser.openapi import Endpoint + body = oai.RequestBody.construct(content={}) + schemas = Schemas() - result = Endpoint.parse_multipart_body(body=body, config=MagicMock()) + prop, schemas = Endpoint.parse_multipart_body( + body=body, schemas=schemas, parent_name="parent", config=MagicMock() + ) - assert result is None + assert prop is None def test_parse_request_json_body(self, mocker): from openapi_python_client.parser.openapi import Endpoint, Schemas @@ -230,7 +280,7 @@ def test_add_body_no_data(self, mocker): parse_request_form_body.assert_not_called() - def test_add_body_bad_data(self, mocker): + def test_add_body_bad_json_data(self, mocker): from openapi_python_client.parser.openapi import Endpoint, Schemas mocker.patch.object(Endpoint, "parse_request_form_body") @@ -250,7 +300,35 @@ def test_add_body_bad_data(self, mocker): assert result == ( ParseError( - header=f"Cannot parse body of endpoint {endpoint.name}", + header=f"Cannot parse JSON body of endpoint {endpoint.name}", + detail=parse_error.detail, + data=parse_error.data, + ), + other_schemas, + ) + + def test_add_body_bad_multipart_data(self, mocker): + from openapi_python_client.parser.openapi import Endpoint, Schemas + + mocker.patch.object(Endpoint, "parse_request_form_body") + mocker.patch.object(Endpoint, "parse_request_json_body", return_value=(mocker.MagicMock(), mocker.MagicMock())) + parse_error = ParseError(data=mocker.MagicMock(), detail=mocker.MagicMock()) + other_schemas = mocker.MagicMock() + mocker.patch.object(Endpoint, "parse_multipart_body", return_value=(parse_error, other_schemas)) + endpoint = self.make_endpoint() + request_body = mocker.MagicMock() + schemas = Schemas() + + result = Endpoint._add_body( + endpoint=endpoint, + data=oai.Operation.construct(requestBody=request_body), + schemas=schemas, + config=MagicMock(), + ) + + assert result == ( + ParseError( + header=f"Cannot parse multipart body of endpoint {endpoint.name}", detail=parse_error.detail, data=parse_error.data, ), @@ -264,20 +342,24 @@ def test_add_body_happy(self, mocker): request_body = mocker.MagicMock() config = mocker.MagicMock() form_body_class = Class(name="A", module_name="a") - multipart_body_class = Class(name="B", module_name="b") parse_request_form_body = mocker.patch.object(Endpoint, "parse_request_form_body", return_value=form_body_class) - parse_multipart_body = mocker.patch.object(Endpoint, "parse_multipart_body", return_value=multipart_body_class) + + multipart_body = mocker.MagicMock(autospec=Property) + multipart_body_imports = mocker.MagicMock() + multipart_body.get_imports.return_value = {multipart_body_imports} + multipart_schemas = mocker.MagicMock() + parse_multipart_body = mocker.patch.object( + Endpoint, "parse_multipart_body", return_value=(multipart_body, multipart_schemas) + ) json_body = mocker.MagicMock(autospec=Property) json_body_imports = mocker.MagicMock() json_body.get_imports.return_value = {json_body_imports} - parsed_schemas = mocker.MagicMock() + json_schemas = mocker.MagicMock() parse_request_json_body = mocker.patch.object( - Endpoint, "parse_request_json_body", return_value=(json_body, parsed_schemas) - ) - import_string_from_class = mocker.patch( - f"{MODULE_NAME}.import_string_from_class", side_effect=["import_1", "import_2"] + Endpoint, "parse_request_json_body", return_value=(json_body, json_schemas) ) + import_string_from_class = mocker.patch(f"{MODULE_NAME}.import_string_from_class", return_value="import_1") endpoint = self.make_endpoint() initial_schemas = mocker.MagicMock() @@ -289,23 +371,21 @@ def test_add_body_happy(self, mocker): config=config, ) - assert response_schemas == parsed_schemas + assert response_schemas == multipart_schemas parse_request_form_body.assert_called_once_with(body=request_body, config=config) parse_request_json_body.assert_called_once_with( body=request_body, schemas=initial_schemas, parent_name="name", config=config ) - parse_multipart_body.assert_called_once_with(body=request_body, config=config) - import_string_from_class.assert_has_calls( - [ - mocker.call(form_body_class, prefix="...models"), - mocker.call(multipart_body_class, prefix="...models"), - ] + parse_multipart_body.assert_called_once_with( + body=request_body, schemas=json_schemas, parent_name="name", config=config ) + import_string_from_class.assert_called_once_with(form_body_class, prefix="...models") json_body.get_imports.assert_called_once_with(prefix="...") - assert endpoint.relative_imports == {"import_1", "import_2", "import_3", json_body_imports} + multipart_body.get_imports.assert_called_once_with(prefix="...") + assert endpoint.relative_imports == {"import_1", "import_3", json_body_imports, multipart_body_imports} assert endpoint.json_body == json_body assert endpoint.form_body_class == form_body_class - assert endpoint.multipart_body_class == multipart_body_class + assert endpoint.multipart_body == multipart_body def test__add_responses_status_code_error(self, mocker): from openapi_python_client.parser.openapi import Endpoint, Schemas From 1577d79328bb616747c38ac4d4805fac43bc47c5 Mon Sep 17 00:00:00 2001 From: Constantinos Symeonides Date: Mon, 17 May 2021 22:10:19 +0100 Subject: [PATCH 07/10] fix: Declaring types is necessary --- .../models/body_upload_file_tests_upload_post.py | 12 ++++++------ openapi_python_client/templates/model.py.jinja | 4 ++-- .../property_templates/list_property.py.jinja | 12 ++++++++++-- .../property_templates/model_property.py.jinja | 6 ++++-- 4 files changed, 22 insertions(+), 12 deletions(-) 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 52a490529..4f501765e 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 @@ -1,6 +1,6 @@ import json from io import BytesIO -from typing import Any, Dict, List, Optional, Type, TypeVar, Union, cast +from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union, cast import attr @@ -76,18 +76,18 @@ def to_multipart(self) -> Dict[str, Any]: some_object = (None, json.dumps(self.some_object.to_dict()), "application/json") - some_optional_file = UNSET + some_optional_file: Union[Unset, FileJsonType] = UNSET if not isinstance(self.some_optional_file, Unset): some_optional_file = self.some_optional_file.to_tuple() some_string = self.some_string some_number = self.some_number - some_array = UNSET + some_array: Union[Unset, Tuple[None, str, str]] = UNSET if not isinstance(self.some_array, Unset): - some_array = self.some_array - some_array = (None, json.dumps(some_array), "application/json") + _temp_some_array = self.some_array + some_array = (None, json.dumps(_temp_some_array), "application/json") - some_optional_object = UNSET + some_optional_object: Union[Unset, Tuple[None, str, str]] = UNSET if not isinstance(self.some_optional_object, Unset): some_optional_object = (None, json.dumps(self.some_optional_object.to_dict()), "application/json") diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index a2deadf81..dd3e4de5a 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -47,7 +47,7 @@ class {{ class_name }}: {% for property in model.required_properties + model.optional_properties %} {% if property.template %} {% from "property_templates/" + property.template import transform %} -{{ transform(property, "self." + property.python_name, property.python_name, declare_type=not multipart, stringify=multipart) }} +{{ transform(property, "self." + property.python_name, property.python_name, stringify=multipart) }} {% else %} {{ property.python_name }} = self.{{ property.python_name }} {% endif %} @@ -58,7 +58,7 @@ field_dict: Dict[str, Any] = {} {% if model.additional_properties.template %} {% from "property_templates/" + model.additional_properties.template import transform %} for prop_name, prop in self.additional_properties.items(): - {{ transform(model.additional_properties, "prop", "field_dict[prop_name]", declare_type=not multipart, stringify=multipart) | indent(4) }} + {{ transform(model.additional_properties, "prop", "field_dict[prop_name]", stringify=multipart) | indent(4) }} {% else %} field_dict.update(self.additional_properties) {% endif %} 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 ba9926868..e872e9d0d 100644 --- a/openapi_python_client/templates/property_templates/list_property.py.jinja +++ b/openapi_python_client/templates/property_templates/list_property.py.jinja @@ -19,6 +19,10 @@ for {{ inner_source }} in (_{{ property.python_name }} or []): {% macro _transform(property, source, destination, stringify) %} {% set inner_property = property.inner_property %} +{% if stringify %} +{% set stringified_destination = destination %} +{% set destination = "_temp_" + destination %} +{% endif %} {% if inner_property.template %} {% set inner_source = inner_property.python_name + "_data" %} {{ destination }} = [] @@ -30,7 +34,7 @@ for {{ inner_source }} in {{ source }}: {{ destination }} = {{ source }} {% endif %} {% if stringify %} -{{ destination }} = (None, json.dumps({{ destination }}), 'application/json') +{{ stringified_destination }} = (None, json.dumps({{ destination }}), 'application/json') {% endif %} {% endmacro %} @@ -38,6 +42,10 @@ for {{ inner_source }} in {{ source }}: {% macro transform(property, source, destination, declare_type=True, stringify=False) %} {% set inner_property = property.inner_property %} +{% set type_string = property.get_type_string(json=True) %} +{% if stringify %} + {% set type_string = "Union[Unset, Tuple[None, str, str]]" %} +{% endif %} {% if property.required %} {% if property.nullable %} if {{ source }} is None: @@ -48,7 +56,7 @@ else: {{ _transform(property, source, destination, stringify) }} {% endif %} {% else %} -{{ destination }}{% if declare_type %}: {{ property.get_type_string(json=True) }}{% endif %} = UNSET +{{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET if not isinstance({{ source }}, Unset): {% if property.nullable %} if {{ source }} is None: 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 660317e41..0e99e0e57 100644 --- a/openapi_python_client/templates/property_templates/model_property.py.jinja +++ b/openapi_python_client/templates/property_templates/model_property.py.jinja @@ -12,8 +12,10 @@ {% macro transform(property, source, destination, declare_type=True, stringify=False, transform_method="to_dict") %} {% set transformed = source + "." + transform_method + "()" %} +{% set type_string = property.get_type_string(json=True) %} {% if stringify %} -{% set transformed = "(None, json.dumps(" + transformed + "), 'application/json')" %} + {% set transformed = "(None, json.dumps(" + transformed + "), 'application/json')" %} + {% set type_string = "Union[Unset, Tuple[None, str, str]]" %} {% endif %} {% if property.required %} {% if property.nullable %} @@ -22,7 +24,7 @@ {{ destination }} = {{ transformed }} {% endif %} {% else %} -{{ destination }}{% if declare_type %}: {{ property.get_type_string(json=True) }}{% endif %} = UNSET +{{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET if not isinstance({{ source }}, Unset): {% if property.nullable %} {{ destination }} = {{ transformed }} if {{ source }} else None From 9ece4eb4c60b579043418d0238dd520604a34e08 Mon Sep 17 00:00:00 2001 From: Constantinos Symeonides Date: Tue, 18 May 2021 10:47:46 +0100 Subject: [PATCH 08/10] fix: Remove dead code --- end_to_end_tests/golden-record/my_test_api_client/types.py | 2 +- openapi_python_client/templates/types.py.jinja | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/end_to_end_tests/golden-record/my_test_api_client/types.py b/end_to_end_tests/golden-record/my_test_api_client/types.py index c47d4f1e3..a6f00ece9 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/types.py +++ b/end_to_end_tests/golden-record/my_test_api_client/types.py @@ -40,4 +40,4 @@ class Response(Generic[T]): parsed: Optional[T] -__all__ = ["File", "Response", "is_file", "FileJsonType"] +__all__ = ["File", "Response", "FileJsonType"] diff --git a/openapi_python_client/templates/types.py.jinja b/openapi_python_client/templates/types.py.jinja index c167cb7cc..70daf2af4 100644 --- a/openapi_python_client/templates/types.py.jinja +++ b/openapi_python_client/templates/types.py.jinja @@ -41,4 +41,4 @@ class Response(Generic[T]): parsed: Optional[T] -__all__ = ["File", "Response", "is_file", "FileJsonType"] +__all__ = ["File", "Response", "FileJsonType"] From 57c42a886a61f546b24803306f35839b9d169826 Mon Sep 17 00:00:00 2001 From: Constantinos Symeonides Date: Wed, 19 May 2021 12:33:26 +0100 Subject: [PATCH 09/10] fix: Non-JSON non-file multipart props need to be text/plain --- .../my_test_api_client/models/__init__.py | 1 + .../body_upload_file_tests_upload_post.py | 61 ++++++++++++++++++- ...e_tests_upload_post_additional_property.py | 54 ++++++++++++++++ end_to_end_tests/openapi.json | 12 +++- .../templates/model.py.jinja | 7 +++ .../property_templates/enum_property.py.jinja | 16 +++-- 6 files changed, 143 insertions(+), 8 deletions(-) create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_additional_property.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py index 575a68e67..c71152ef6 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py @@ -9,6 +9,7 @@ from .an_int_enum import AnIntEnum from .another_all_of_sub_model import AnotherAllOfSubModel from .body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost +from .body_upload_file_tests_upload_post_additional_property import BodyUploadFileTestsUploadPostAdditionalProperty from .body_upload_file_tests_upload_post_some_nullable_object import BodyUploadFileTestsUploadPostSomeNullableObject from .body_upload_file_tests_upload_post_some_object import BodyUploadFileTestsUploadPostSomeObject from .body_upload_file_tests_upload_post_some_optional_object import BodyUploadFileTestsUploadPostSomeOptionalObject 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 4f501765e..683025d4e 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 @@ -4,6 +4,9 @@ import attr +from ..models.body_upload_file_tests_upload_post_additional_property import ( + BodyUploadFileTestsUploadPostAdditionalProperty, +) from ..models.body_upload_file_tests_upload_post_some_nullable_object import ( BodyUploadFileTestsUploadPostSomeNullableObject, ) @@ -11,6 +14,7 @@ from ..models.body_upload_file_tests_upload_post_some_optional_object import ( BodyUploadFileTestsUploadPostSomeOptionalObject, ) +from ..models.different_enum import DifferentEnum from ..types import UNSET, File, FileJsonType, Unset T = TypeVar("T", bound="BodyUploadFileTestsUploadPost") @@ -28,6 +32,10 @@ class BodyUploadFileTestsUploadPost: some_number: Union[Unset, float] = UNSET some_array: Union[Unset, List[float]] = UNSET some_optional_object: Union[Unset, BodyUploadFileTestsUploadPostSomeOptionalObject] = UNSET + some_enum: Union[Unset, DifferentEnum] = UNSET + additional_properties: Dict[str, BodyUploadFileTestsUploadPostAdditionalProperty] = attr.ib( + init=False, factory=dict + ) def to_dict(self) -> Dict[str, Any]: some_file = self.some_file.to_tuple() @@ -50,7 +58,14 @@ def to_dict(self) -> Dict[str, Any]: some_nullable_object = self.some_nullable_object.to_dict() if self.some_nullable_object else None + some_enum: Union[Unset, str] = UNSET + if not isinstance(self.some_enum, Unset): + some_enum = self.some_enum.value + field_dict: Dict[str, Any] = {} + for prop_name, prop in self.additional_properties.items(): + field_dict[prop_name] = prop.to_dict() + field_dict.update( { "some_file": some_file, @@ -68,6 +83,8 @@ def to_dict(self) -> Dict[str, Any]: field_dict["some_array"] = some_array if some_optional_object is not UNSET: field_dict["some_optional_object"] = some_optional_object + if some_enum is not UNSET: + field_dict["some_enum"] = some_enum return field_dict @@ -80,8 +97,8 @@ def to_multipart(self) -> Dict[str, Any]: if not isinstance(self.some_optional_file, Unset): some_optional_file = self.some_optional_file.to_tuple() - some_string = self.some_string - some_number = self.some_number + some_string = self.some_string if self.some_string is UNSET else (None, str(self.some_string), "text/plain") + some_number = self.some_number if self.some_number is UNSET else (None, str(self.some_number), "text/plain") some_array: Union[Unset, Tuple[None, str, str]] = UNSET if not isinstance(self.some_array, Unset): _temp_some_array = self.some_array @@ -97,7 +114,14 @@ def to_multipart(self) -> Dict[str, Any]: else None ) + some_enum: Union[Unset, Tuple[None, str, str]] = UNSET + if not isinstance(self.some_enum, Unset): + some_enum = (None, str(self.some_enum.value), "text/plain") + field_dict: Dict[str, Any] = {} + for prop_name, prop in self.additional_properties.items(): + field_dict[prop_name] = (None, json.dumps(prop.to_dict()), "application/json") + field_dict.update( { "some_file": some_file, @@ -115,6 +139,8 @@ def to_multipart(self) -> Dict[str, Any]: field_dict["some_array"] = some_array if some_optional_object is not UNSET: field_dict["some_optional_object"] = some_optional_object + if some_enum is not UNSET: + field_dict["some_enum"] = some_enum return field_dict @@ -152,6 +178,13 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: else: some_nullable_object = BodyUploadFileTestsUploadPostSomeNullableObject.from_dict(_some_nullable_object) + _some_enum = d.pop("some_enum", UNSET) + some_enum: Union[Unset, DifferentEnum] + if isinstance(_some_enum, Unset): + some_enum = UNSET + else: + some_enum = DifferentEnum(_some_enum) + body_upload_file_tests_upload_post = cls( some_file=some_file, some_object=some_object, @@ -161,6 +194,30 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: some_array=some_array, some_optional_object=some_optional_object, some_nullable_object=some_nullable_object, + some_enum=some_enum, ) + additional_properties = {} + for prop_name, prop_dict in d.items(): + additional_property = BodyUploadFileTestsUploadPostAdditionalProperty.from_dict(prop_dict) + + additional_properties[prop_name] = additional_property + + body_upload_file_tests_upload_post.additional_properties = additional_properties return body_upload_file_tests_upload_post + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> BodyUploadFileTestsUploadPostAdditionalProperty: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: BodyUploadFileTestsUploadPostAdditionalProperty) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_additional_property.py b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_additional_property.py new file mode 100644 index 000000000..b2ce8457e --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post_additional_property.py @@ -0,0 +1,54 @@ +from typing import Any, Dict, List, Type, TypeVar, Union + +import attr + +from ..types import UNSET, Unset + +T = TypeVar("T", bound="BodyUploadFileTestsUploadPostAdditionalProperty") + + +@attr.s(auto_attribs=True) +class BodyUploadFileTestsUploadPostAdditionalProperty: + """ """ + + foo: Union[Unset, str] = UNSET + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + foo = self.foo + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + if foo is not UNSET: + field_dict["foo"] = foo + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + foo = d.pop("foo", UNSET) + + body_upload_file_tests_upload_post_additional_property = cls( + foo=foo, + ) + + body_upload_file_tests_upload_post_additional_property.additional_properties = d + return body_upload_file_tests_upload_post_additional_property + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index 44d6eaa67..4d40f108e 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -1114,9 +1114,19 @@ "type": "string" } } + }, + "some_enum": { + "$ref": "#/components/schemas/DifferentEnum" } }, - "additionalProperties": false + "additionalProperties": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + } }, "DifferentEnum": { "title": "DifferentEnum", diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index dd3e4de5a..c4c23c878 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -48,6 +48,8 @@ class {{ class_name }}: {% if property.template %} {% from "property_templates/" + property.template import transform %} {{ transform(property, "self." + property.python_name, property.python_name, stringify=multipart) }} +{% elif multipart %} +{{ property.python_name }} = self.{{ property.python_name }} if self.{{ property.python_name }} is UNSET else (None, str(self.{{ property.python_name }}), "text/plain") {% else %} {{ property.python_name }} = self.{{ property.python_name }} {% endif %} @@ -59,6 +61,11 @@ field_dict: Dict[str, Any] = {} {% from "property_templates/" + model.additional_properties.template import transform %} for prop_name, prop in self.additional_properties.items(): {{ transform(model.additional_properties, "prop", "field_dict[prop_name]", stringify=multipart) | indent(4) }} +{% elif multipart %} +field_dict.update({ + key: (None, str(value), "text/plain") + for key, value in self.additional_properties.items() +}) {% else %} field_dict.update(self.additional_properties) {% endif %} 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 46e1ebe35..340d67359 100644 --- a/openapi_python_client/templates/property_templates/enum_property.py.jinja +++ b/openapi_python_client/templates/property_templates/enum_property.py.jinja @@ -11,19 +11,25 @@ {% macro check_type_for_construct(property, source) %}isinstance({{ source }}, {{ property.value_type.__name__ }}){% endmacro %} {% macro transform(property, source, destination, declare_type=True, stringify=False) %} +{% set transformed = source + ".value" %} +{% set type_string = property.get_type_string(json=True) %} +{% if stringify %} + {% set transformed = "(None, str(" + transformed + "), 'text/plain')" %} + {% set type_string = "Union[Unset, Tuple[None, str, str]]" %} +{% endif %} {% if property.required %} {% if property.nullable %} -{{ destination }} = {{ source }}.value if {{ source }} else None +{{ destination }} = {{ transformed }} if {{ source }} else None {% else %} -{{ destination }} = {{ source }}.value +{{ destination }} = {{ transformed }} {% endif %} {% else %} -{{ destination }}{% if declare_type %}: {{ property.get_type_string(json=True) }}{% endif %} = UNSET +{{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET if not isinstance({{ source }}, Unset): {% if property.nullable %} - {{ destination }} = {{ source }}.value if {{ source }} else None + {{ destination }} = {{ transformed }} if {{ source }} else None {% else %} - {{ destination }} = {{ source }}.value + {{ destination }} = {{ transformed }} {% endif %} {% endif %} {% endmacro %} From 7cbeb7e178ceb2464a0ccd52a039134994774da3 Mon Sep 17 00:00:00 2001 From: Constantinos Symeonides Date: Tue, 25 May 2021 10:40:12 +0100 Subject: [PATCH 10/10] refactor: Avoid breaking change --- .../tests/upload_file_tests_upload_post.py | 22 +++++++++---------- openapi_python_client/parser/openapi.py | 2 +- .../templates/endpoint_macros.py.jinja | 4 ++-- .../property_templates/list_property.py.jinja | 3 ++- .../model_property.py.jinja | 3 ++- tests/test_parser/test_openapi.py | 4 ++-- 6 files changed, 20 insertions(+), 18 deletions(-) diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py index e0c29fd74..4b2d294cb 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py @@ -11,7 +11,7 @@ def _get_kwargs( *, client: Client, - multipart_body: BodyUploadFileTestsUploadPost, + multipart_data: BodyUploadFileTestsUploadPost, keep_alive: Union[Unset, bool] = UNSET, ) -> Dict[str, Any]: url = "{}/tests/upload".format(client.base_url) @@ -22,14 +22,14 @@ def _get_kwargs( if keep_alive is not UNSET: headers["keep-alive"] = keep_alive - multipart_multipart_body = multipart_body.to_multipart() + multipart_multipart_data = multipart_data.to_multipart() return { "url": url, "headers": headers, "cookies": cookies, "timeout": client.get_timeout(), - "files": multipart_multipart_body, + "files": multipart_multipart_data, } @@ -57,12 +57,12 @@ def _build_response(*, response: httpx.Response) -> Response[Union[HTTPValidatio def sync_detailed( *, client: Client, - multipart_body: BodyUploadFileTestsUploadPost, + multipart_data: BodyUploadFileTestsUploadPost, keep_alive: Union[Unset, bool] = UNSET, ) -> Response[Union[HTTPValidationError, None]]: kwargs = _get_kwargs( client=client, - multipart_body=multipart_body, + multipart_data=multipart_data, keep_alive=keep_alive, ) @@ -76,14 +76,14 @@ def sync_detailed( def sync( *, client: Client, - multipart_body: BodyUploadFileTestsUploadPost, + multipart_data: BodyUploadFileTestsUploadPost, keep_alive: Union[Unset, bool] = UNSET, ) -> Optional[Union[HTTPValidationError, None]]: """Upload a file""" return sync_detailed( client=client, - multipart_body=multipart_body, + multipart_data=multipart_data, keep_alive=keep_alive, ).parsed @@ -91,12 +91,12 @@ def sync( async def asyncio_detailed( *, client: Client, - multipart_body: BodyUploadFileTestsUploadPost, + multipart_data: BodyUploadFileTestsUploadPost, keep_alive: Union[Unset, bool] = UNSET, ) -> Response[Union[HTTPValidationError, None]]: kwargs = _get_kwargs( client=client, - multipart_body=multipart_body, + multipart_data=multipart_data, keep_alive=keep_alive, ) @@ -109,7 +109,7 @@ async def asyncio_detailed( async def asyncio( *, client: Client, - multipart_body: BodyUploadFileTestsUploadPost, + multipart_data: BodyUploadFileTestsUploadPost, keep_alive: Union[Unset, bool] = UNSET, ) -> Optional[Union[HTTPValidationError, None]]: """Upload a file""" @@ -117,7 +117,7 @@ async def asyncio( return ( await asyncio_detailed( client=client, - multipart_body=multipart_body, + multipart_data=multipart_data, keep_alive=keep_alive, ) ).parsed diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index 8ea8d4afd..a1bd5489b 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -116,7 +116,7 @@ def parse_multipart_body( multipart_body = body_content.get("multipart/form-data") if multipart_body is not None and multipart_body.media_type_schema is not None: prop, schemas = property_from_data( - name="multipart_body", + name="multipart_data", required=True, data=multipart_body.media_type_schema, schemas=schemas, diff --git a/openapi_python_client/templates/endpoint_macros.py.jinja b/openapi_python_client/templates/endpoint_macros.py.jinja index 3723a2f60..add4c68b2 100644 --- a/openapi_python_client/templates/endpoint_macros.py.jinja +++ b/openapi_python_client/templates/endpoint_macros.py.jinja @@ -101,7 +101,7 @@ form_data: {{ endpoint.form_body_class.name }}, {% endif %} {# Multipart data if any #} {% if endpoint.multipart_body %} -multipart_body: {{ endpoint.multipart_body.get_type_string() }}, +multipart_data: {{ endpoint.multipart_body.get_type_string() }}, {% endif %} {# JSON body if any #} {% if endpoint.json_body %} @@ -130,7 +130,7 @@ client=client, form_data=form_data, {% endif %} {% if endpoint.multipart_body %} -multipart_body=multipart_body, +multipart_data=multipart_data, {% endif %} {% if endpoint.json_body %} json_body=json_body, 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 e872e9d0d..b955ad40a 100644 --- a/openapi_python_client/templates/property_templates/list_property.py.jinja +++ b/openapi_python_client/templates/property_templates/list_property.py.jinja @@ -42,9 +42,10 @@ for {{ inner_source }} in {{ source }}: {% macro transform(property, source, destination, declare_type=True, stringify=False) %} {% set inner_property = property.inner_property %} -{% set type_string = property.get_type_string(json=True) %} {% if stringify %} {% set type_string = "Union[Unset, Tuple[None, str, str]]" %} +{% else %} + {% set type_string = property.get_type_string(json=True) %} {% endif %} {% if property.required %} {% if property.nullable %} 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 0e99e0e57..b5b986863 100644 --- a/openapi_python_client/templates/property_templates/model_property.py.jinja +++ b/openapi_python_client/templates/property_templates/model_property.py.jinja @@ -12,10 +12,11 @@ {% macro transform(property, source, destination, declare_type=True, stringify=False, transform_method="to_dict") %} {% set transformed = source + "." + transform_method + "()" %} -{% set type_string = property.get_type_string(json=True) %} {% if stringify %} {% set transformed = "(None, json.dumps(" + transformed + "), 'application/json')" %} {% set type_string = "Union[Unset, Tuple[None, str, str]]" %} +{% else %} + {% set type_string = property.get_type_string(json=True) %} {% endif %} {% if property.required %} {% if property.nullable %} diff --git a/tests/test_parser/test_openapi.py b/tests/test_parser/test_openapi.py index a7bbdec28..05e8d4255 100644 --- a/tests/test_parser/test_openapi.py +++ b/tests/test_parser/test_openapi.py @@ -187,7 +187,7 @@ def test_parse_multipart_body(self, mocker, model_property_factory): result = Endpoint.parse_multipart_body(body=body, schemas=schemas_before, parent_name="parent", config=config) property_from_data.assert_called_once_with( - name="multipart_body", + name="multipart_data", required=True, data=schema, schemas=schemas_before, @@ -218,7 +218,7 @@ def test_parse_multipart_body_existing_schema(self, mocker, model_property_facto result = Endpoint.parse_multipart_body(body=body, schemas=schemas_before, parent_name="parent", config=config) property_from_data.assert_called_once_with( - name="multipart_body", + name="multipart_data", required=True, data=schema, schemas=schemas_before,