From 6ae8a8f4c7b00ed15bbb3cfdc49a7b99dbb56c4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=B5=E3=81=81?= <34892635+fa0311@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:15:50 +0900 Subject: [PATCH] [python] fix deserialize on basic str fails (#18800) * [python] fix #18774 Deserialize on basic str fails * [python] update sample * [python] update test * [python] remove type * [python] fix test * [python] add top level type test * Update deserialize content_type parameter and quote * [python] restore echo_api test * [python] add allow empty json in Response --- .../main/resources/python/api_client.mustache | 27 +++++-- .../openapi_client/api_client.py | 27 +++++-- .../tests/test_manual.py | 5 +- .../python/openapi_client/api_client.py | 27 +++++-- .../echo_api/python/tests/test_manual.py | 8 +- .../python-aiohttp/petstore_api/api_client.py | 27 +++++-- .../python/petstore_api/api_client.py | 27 +++++-- .../client/petstore/python/tests/test_api.py | 6 ++ .../python/tests/test_deserialization.py | 30 ++++---- .../petstore/python/tests/test_fake_api.py | 77 ++++++++++++++++++- 10 files changed, 197 insertions(+), 64 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/python/api_client.mustache b/modules/openapi-generator/src/main/resources/python/api_client.mustache index 97b8f8d84c72..4d1d789e4e85 100644 --- a/modules/openapi-generator/src/main/resources/python/api_client.mustache +++ b/modules/openapi-generator/src/main/resources/python/api_client.mustache @@ -322,10 +322,7 @@ class ApiClient: match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type) encoding = match.group(1) if match else "utf-8" response_text = response_data.data.decode(encoding) - if response_type in ["bytearray", "str"]: - return_data = self.__deserialize_primitive(response_text, response_type) - else: - return_data = self.deserialize(response_text, response_type) + return_data = self.deserialize(response_text, response_type, content_type) finally: if not 200 <= response_data.status <= 299: raise ApiException.from_response( @@ -393,21 +390,35 @@ class ApiClient: for key, val in obj_dict.items() } - def deserialize(self, response_text, response_type): + def deserialize(self, response_text: str, response_type: str, content_type: Optional[str]): """Deserializes response into an object. :param response: RESTResponse object to be deserialized. :param response_type: class literal for deserialized object, or string of class name. + :param content_type: content type of response. :return: deserialized object. """ # fetch data from response object - try: - data = json.loads(response_text) - except ValueError: + if content_type is None: + try: + data = json.loads(response_text) + except ValueError: + data = response_text + elif content_type.startswith("application/json"): + if response_text == "": + data = "" + else: + data = json.loads(response_text) + elif content_type.startswith("text/plain"): data = response_text + else: + raise ApiException( + status=0, + reason="Unsupported content type: {0}".format(content_type) + ) return self.__deserialize(data, response_type) diff --git a/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/openapi_client/api_client.py b/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/openapi_client/api_client.py index 5e39f780d88a..2d5d7b55ad7d 100644 --- a/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/openapi_client/api_client.py +++ b/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/openapi_client/api_client.py @@ -315,10 +315,7 @@ class ApiClient: match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type) encoding = match.group(1) if match else "utf-8" response_text = response_data.data.decode(encoding) - if response_type in ["bytearray", "str"]: - return_data = self.__deserialize_primitive(response_text, response_type) - else: - return_data = self.deserialize(response_text, response_type) + return_data = self.deserialize(response_text, response_type, content_type) finally: if not 200 <= response_data.status <= 299: raise ApiException.from_response( @@ -386,21 +383,35 @@ class ApiClient: for key, val in obj_dict.items() } - def deserialize(self, response_text, response_type): + def deserialize(self, response_text: str, response_type: str, content_type: Optional[str]): """Deserializes response into an object. :param response: RESTResponse object to be deserialized. :param response_type: class literal for deserialized object, or string of class name. + :param content_type: content type of response. :return: deserialized object. """ # fetch data from response object - try: - data = json.loads(response_text) - except ValueError: + if content_type is None: + try: + data = json.loads(response_text) + except ValueError: + data = response_text + elif content_type.startswith("application/json"): + if response_text == "": + data = "" + else: + data = json.loads(response_text) + elif content_type.startswith("text/plain"): data = response_text + else: + raise ApiException( + status=0, + reason="Unsupported content type: {0}".format(content_type) + ) return self.__deserialize(data, response_type) diff --git a/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/tests/test_manual.py b/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/tests/test_manual.py index 4fbe46dd563e..1a5d0bb4d83f 100644 --- a/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/tests/test_manual.py +++ b/samples/client/echo_api/python-disallowAdditionalPropertiesIfNotPresent/tests/test_manual.py @@ -113,15 +113,12 @@ class TestManual(unittest.TestCase): n = openapi_client.Pet.from_dict({"name": "testing", "photoUrls": ["http://1", "http://2"]}) api_instance = openapi_client.BodyApi() api_response = api_instance.test_echo_body_pet_response_string(n) - self.assertEqual(api_response, '{"name": "testing", "photoUrls": ["http://1", "http://2"]}') + self.assertEqual(api_response, "{'name': 'testing', 'photoUrls': ['http://1', 'http://2']}") t = openapi_client.Tag() api_response = api_instance.test_echo_body_tag_response_string(t) self.assertEqual(api_response, "{}") # assertion to ensure {} is sent in the body - api_response = api_instance.test_echo_body_tag_response_string(None) - self.assertEqual(api_response, "") # assertion to ensure emtpy string is sent in the body - api_response = api_instance.test_echo_body_free_form_object_response_string({}) self.assertEqual(api_response, "{}") # assertion to ensure {} is sent in the body diff --git a/samples/client/echo_api/python/openapi_client/api_client.py b/samples/client/echo_api/python/openapi_client/api_client.py index 5e39f780d88a..2d5d7b55ad7d 100644 --- a/samples/client/echo_api/python/openapi_client/api_client.py +++ b/samples/client/echo_api/python/openapi_client/api_client.py @@ -315,10 +315,7 @@ class ApiClient: match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type) encoding = match.group(1) if match else "utf-8" response_text = response_data.data.decode(encoding) - if response_type in ["bytearray", "str"]: - return_data = self.__deserialize_primitive(response_text, response_type) - else: - return_data = self.deserialize(response_text, response_type) + return_data = self.deserialize(response_text, response_type, content_type) finally: if not 200 <= response_data.status <= 299: raise ApiException.from_response( @@ -386,21 +383,35 @@ class ApiClient: for key, val in obj_dict.items() } - def deserialize(self, response_text, response_type): + def deserialize(self, response_text: str, response_type: str, content_type: Optional[str]): """Deserializes response into an object. :param response: RESTResponse object to be deserialized. :param response_type: class literal for deserialized object, or string of class name. + :param content_type: content type of response. :return: deserialized object. """ # fetch data from response object - try: - data = json.loads(response_text) - except ValueError: + if content_type is None: + try: + data = json.loads(response_text) + except ValueError: + data = response_text + elif content_type.startswith("application/json"): + if response_text == "": + data = "" + else: + data = json.loads(response_text) + elif content_type.startswith("text/plain"): data = response_text + else: + raise ApiException( + status=0, + reason="Unsupported content type: {0}".format(content_type) + ) return self.__deserialize(data, response_type) diff --git a/samples/client/echo_api/python/tests/test_manual.py b/samples/client/echo_api/python/tests/test_manual.py index 42ae3aafd85e..e3a3771832a2 100644 --- a/samples/client/echo_api/python/tests/test_manual.py +++ b/samples/client/echo_api/python/tests/test_manual.py @@ -174,17 +174,17 @@ class TestManual(unittest.TestCase): n = openapi_client.Pet.from_dict({"name": "testing", "photoUrls": ["http://1", "http://2"]}) api_instance = openapi_client.BodyApi() api_response = api_instance.test_echo_body_pet_response_string(n) - self.assertEqual(api_response, '{"name": "testing", "photoUrls": ["http://1", "http://2"]}') + self.assertEqual(api_response, "{'name': 'testing', 'photoUrls': ['http://1', 'http://2']}") t = openapi_client.Tag() api_response = api_instance.test_echo_body_tag_response_string(t) self.assertEqual(api_response, "{}") # assertion to ensure {} is sent in the body - api_response = api_instance.test_echo_body_tag_response_string(None) - self.assertEqual(api_response, "") # assertion to ensure emtpy string is sent in the body - api_response = api_instance.test_echo_body_free_form_object_response_string({}) self.assertEqual(api_response, "{}") # assertion to ensure {} is sent in the body + + api_response = api_instance.test_echo_body_tag_response_string(None) + self.assertEqual(api_response, "") # assertion to ensure emtpy string is sent in the body def testAuthHttpBasic(self): api_instance = openapi_client.AuthApi() diff --git a/samples/openapi3/client/petstore/python-aiohttp/petstore_api/api_client.py b/samples/openapi3/client/petstore/python-aiohttp/petstore_api/api_client.py index 991b0d96cc1d..5766c2842237 100644 --- a/samples/openapi3/client/petstore/python-aiohttp/petstore_api/api_client.py +++ b/samples/openapi3/client/petstore/python-aiohttp/petstore_api/api_client.py @@ -317,10 +317,7 @@ class ApiClient: match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type) encoding = match.group(1) if match else "utf-8" response_text = response_data.data.decode(encoding) - if response_type in ["bytearray", "str"]: - return_data = self.__deserialize_primitive(response_text, response_type) - else: - return_data = self.deserialize(response_text, response_type) + return_data = self.deserialize(response_text, response_type, content_type) finally: if not 200 <= response_data.status <= 299: raise ApiException.from_response( @@ -388,21 +385,35 @@ class ApiClient: for key, val in obj_dict.items() } - def deserialize(self, response_text, response_type): + def deserialize(self, response_text: str, response_type: str, content_type: Optional[str]): """Deserializes response into an object. :param response: RESTResponse object to be deserialized. :param response_type: class literal for deserialized object, or string of class name. + :param content_type: content type of response. :return: deserialized object. """ # fetch data from response object - try: - data = json.loads(response_text) - except ValueError: + if content_type is None: + try: + data = json.loads(response_text) + except ValueError: + data = response_text + elif content_type.startswith("application/json"): + if response_text == "": + data = "" + else: + data = json.loads(response_text) + elif content_type.startswith("text/plain"): data = response_text + else: + raise ApiException( + status=0, + reason="Unsupported content type: {0}".format(content_type) + ) return self.__deserialize(data, response_type) diff --git a/samples/openapi3/client/petstore/python/petstore_api/api_client.py b/samples/openapi3/client/petstore/python/petstore_api/api_client.py index 7e17903a95fd..c4dbcb450a7b 100755 --- a/samples/openapi3/client/petstore/python/petstore_api/api_client.py +++ b/samples/openapi3/client/petstore/python/petstore_api/api_client.py @@ -314,10 +314,7 @@ class ApiClient: match = re.search(r"charset=([a-zA-Z\-\d]+)[\s;]?", content_type) encoding = match.group(1) if match else "utf-8" response_text = response_data.data.decode(encoding) - if response_type in ["bytearray", "str"]: - return_data = self.__deserialize_primitive(response_text, response_type) - else: - return_data = self.deserialize(response_text, response_type) + return_data = self.deserialize(response_text, response_type, content_type) finally: if not 200 <= response_data.status <= 299: raise ApiException.from_response( @@ -385,21 +382,35 @@ class ApiClient: for key, val in obj_dict.items() } - def deserialize(self, response_text, response_type): + def deserialize(self, response_text: str, response_type: str, content_type: Optional[str]): """Deserializes response into an object. :param response: RESTResponse object to be deserialized. :param response_type: class literal for deserialized object, or string of class name. + :param content_type: content type of response. :return: deserialized object. """ # fetch data from response object - try: - data = json.loads(response_text) - except ValueError: + if content_type is None: + try: + data = json.loads(response_text) + except ValueError: + data = response_text + elif content_type.startswith("application/json"): + if response_text == "": + data = "" + else: + data = json.loads(response_text) + elif content_type.startswith("text/plain"): data = response_text + else: + raise ApiException( + status=0, + reason="Unsupported content type: {0}".format(content_type) + ) return self.__deserialize(data, response_type) diff --git a/samples/openapi3/client/petstore/python/tests/test_api.py b/samples/openapi3/client/petstore/python/tests/test_api.py index 8237969c4943..643e0fc61b5e 100644 --- a/samples/openapi3/client/petstore/python/tests/test_api.py +++ b/samples/openapi3/client/petstore/python/tests/test_api.py @@ -53,6 +53,9 @@ class TestErrorResponsesWithModels(unittest.TestCase): mock_resp.data = json.dumps({"reason400": "400 reason"}).encode("utf-8") mock_resp.getheaders.return_value = {} mock_resp.getheader.return_value = "" + mock_resp.getheader = ( + lambda name: "application/json" if name == "content-type" else Mock() + ) with patch( "petstore_api.api_client.ApiClient.call_api", return_value=mock_resp @@ -71,6 +74,9 @@ class TestErrorResponsesWithModels(unittest.TestCase): mock_resp.data = json.dumps({"reason404": "404 reason"}).encode("utf-8") mock_resp.getheaders.return_value = {} mock_resp.getheader.return_value = "" + mock_resp.getheader = ( + lambda name: "application/json" if name == "content-type" else Mock() + ) with patch( "petstore_api.api_client.ApiClient.call_api", return_value=mock_resp diff --git a/samples/openapi3/client/petstore/python/tests/test_deserialization.py b/samples/openapi3/client/petstore/python/tests/test_deserialization.py index 08e09edea265..8fd654e4bb74 100644 --- a/samples/openapi3/client/petstore/python/tests/test_deserialization.py +++ b/samples/openapi3/client/petstore/python/tests/test_deserialization.py @@ -39,7 +39,7 @@ class DeserializationTests(unittest.TestCase): } response = json.dumps(data) - deserialized = self.deserialize(response, 'Dict[str, EnumTest]') + deserialized = self.deserialize(response, 'Dict[str, EnumTest]', 'application/json') self.assertTrue(isinstance(deserialized, dict)) self.assertTrue(isinstance(deserialized['enum_test'], petstore_api.EnumTest)) self.assertEqual(deserialized['enum_test'], @@ -73,7 +73,7 @@ class DeserializationTests(unittest.TestCase): } response = json.dumps(data) - deserialized = self.deserialize(response, 'Dict[str, Pet]') + deserialized = self.deserialize(response, 'Dict[str, Pet]', 'application/json') self.assertTrue(isinstance(deserialized, dict)) self.assertTrue(isinstance(deserialized['pet'], petstore_api.Pet)) @@ -90,7 +90,7 @@ class DeserializationTests(unittest.TestCase): } response = json.dumps(data) - deserialized = self.deserialize(response, 'Dict[str, Animal]') + deserialized = self.deserialize(response, 'Dict[str, Animal]', 'application/json') self.assertTrue(isinstance(deserialized, dict)) self.assertTrue(isinstance(deserialized['dog'], petstore_api.Dog)) @@ -102,7 +102,7 @@ class DeserializationTests(unittest.TestCase): } response = json.dumps(data) - deserialized = self.deserialize(response, 'Dict[str, int]') + deserialized = self.deserialize(response, 'Dict[str, int]', 'application/json') self.assertTrue(isinstance(deserialized, dict)) self.assertTrue(isinstance(deserialized['integer'], int)) @@ -111,7 +111,7 @@ class DeserializationTests(unittest.TestCase): data = "test str" response = data=json.dumps(data) - deserialized = self.deserialize(response, "str") + deserialized = self.deserialize(response, "str", 'application/json') self.assertTrue(isinstance(deserialized, str)) def test_deserialize_date(self): @@ -119,7 +119,7 @@ class DeserializationTests(unittest.TestCase): data = "1997-07-16" response = data=json.dumps(data) - deserialized = self.deserialize(response, "date") + deserialized = self.deserialize(response, "date", 'application/json') self.assertTrue(isinstance(deserialized, datetime.date)) def test_deserialize_datetime(self): @@ -127,7 +127,7 @@ class DeserializationTests(unittest.TestCase): data = "1997-07-16T19:20:30.45+01:00" response = json.dumps(data) - deserialized = self.deserialize(response, "datetime") + deserialized = self.deserialize(response, "datetime", 'application/json') self.assertTrue(isinstance(deserialized, datetime.datetime)) def test_deserialize_pet(self): @@ -152,7 +152,7 @@ class DeserializationTests(unittest.TestCase): } response = json.dumps(data) - deserialized = self.deserialize(response, "Pet") + deserialized = self.deserialize(response, "Pet", 'application/json') self.assertTrue(isinstance(deserialized, petstore_api.Pet)) self.assertEqual(deserialized.id, 0) self.assertEqual(deserialized.name, "doggie") @@ -202,7 +202,7 @@ class DeserializationTests(unittest.TestCase): }] response = json.dumps(data) - deserialized = self.deserialize(response, "List[Pet]") + deserialized = self.deserialize(response, "List[Pet]", 'application/json') self.assertTrue(isinstance(deserialized, list)) self.assertTrue(isinstance(deserialized[0], petstore_api.Pet)) self.assertEqual(deserialized[0].id, 0) @@ -219,7 +219,7 @@ class DeserializationTests(unittest.TestCase): } response = json.dumps(data) - deserialized = self.deserialize(response, "Dict[str, Dict[str, int]]") + deserialized = self.deserialize(response, "Dict[str, Dict[str, int]]", 'application/json') self.assertTrue(isinstance(deserialized, dict)) self.assertTrue(isinstance(deserialized["foo"], dict)) self.assertTrue(isinstance(deserialized["foo"]["bar"], int)) @@ -229,7 +229,7 @@ class DeserializationTests(unittest.TestCase): data = [["foo"]] response = json.dumps(data) - deserialized = self.deserialize(response, "List[List[str]]") + deserialized = self.deserialize(response, "List[List[str]]", 'application/json') self.assertTrue(isinstance(deserialized, list)) self.assertTrue(isinstance(deserialized[0], list)) self.assertTrue(isinstance(deserialized[0][0], str)) @@ -238,7 +238,7 @@ class DeserializationTests(unittest.TestCase): """ deserialize None """ response = json.dumps(None) - deserialized = self.deserialize(response, "datetime") + deserialized = self.deserialize(response, "datetime", 'application/json') self.assertIsNone(deserialized) def test_deserialize_pig(self): @@ -249,7 +249,7 @@ class DeserializationTests(unittest.TestCase): } response = json.dumps(data) - deserialized = self.deserialize(response, "Pig") + deserialized = self.deserialize(response, "Pig", 'application/json') self.assertTrue(isinstance(deserialized.actual_instance, petstore_api.BasquePig)) self.assertEqual(deserialized.actual_instance.class_name, "BasqueBig") @@ -265,7 +265,7 @@ class DeserializationTests(unittest.TestCase): response = json.dumps(data) with pytest.raises(ValueError) as ex: - deserialized = self.deserialize(response, "Animal") + deserialized = self.deserialize(response, "Animal", 'application/json') assert str( ex.value) == 'Animal failed to lookup discriminator value from {"declawed": true, "className": ' \ '"Cat2222"}. Discriminator property name: className, mapping: {"Cat": "Cat", "Dog": "Dog"}' @@ -277,7 +277,7 @@ class DeserializationTests(unittest.TestCase): response = json.dumps(data) - deserialized = self.deserialize(response, "Animal") + deserialized = self.deserialize(response, "Animal", 'application/json') self.assertTrue(isinstance(deserialized, petstore_api.Cat)) self.assertEqual(deserialized.class_name, "Cat") self.assertEqual(deserialized.declawed, True) diff --git a/samples/openapi3/client/petstore/python/tests/test_fake_api.py b/samples/openapi3/client/petstore/python/tests/test_fake_api.py index dec2dcb4954b..eade99256c5f 100644 --- a/samples/openapi3/client/petstore/python/tests/test_fake_api.py +++ b/samples/openapi3/client/petstore/python/tests/test_fake_api.py @@ -155,7 +155,7 @@ class TestFakeApi(unittest.TestCase): mock_resp.data = b'{"value": "0"}' mock_resp.getheaders.return_value = {} mock_resp.getheader = ( - lambda name: "text/plain" if name == "content-type" else Mock() + lambda name: "application/json" if name == "content-type" else Mock() ) with patch( "petstore_api.api_client.ApiClient.call_api", return_value=mock_resp @@ -165,3 +165,78 @@ class TestFakeApi(unittest.TestCase): param=[OuterEnumInteger.NUMBER_0]) self.assertEqual(call_api_mock.call_args[0][1], 'http://petstore.swagger.io:80/v2/fake/property/enum-int?param=0') + + def testTopLevelStrJson(self): + """Test TopLevelStrJson""" + mock_resp = Mock() + mock_resp.status = 200 + mock_resp.data = b'"a"' + mock_resp.getheaders.return_value = {} + mock_resp.getheader = ( + lambda name: "application/json" if name == "content-type" else Mock() + ) + with patch( + "petstore_api.api_client.ApiClient.call_api", return_value=mock_resp + ): + returned = self.fake_api.fake_return_string() + self.assertEqual('a', returned) + + def testTopLevelIntJson(self): + """Test TopLevelIntJson""" + mock_resp = Mock() + mock_resp.status = 200 + mock_resp.data = b'1' + mock_resp.getheaders.return_value = {} + mock_resp.getheader = ( + lambda name: "application/json" if name == "content-type" else Mock() + ) + with patch( + "petstore_api.api_client.ApiClient.call_api", return_value=mock_resp + ): + returned = self.fake_api.fake_return_int() + self.assertEqual(1, returned) + + def testTopLevelFloatJson(self): + """Test TopLevelFloatJson""" + mock_resp = Mock() + mock_resp.status = 200 + mock_resp.data = b'3.4' + mock_resp.getheaders.return_value = {} + mock_resp.getheader = ( + lambda name: "application/json" if name == "content-type" else Mock() + ) + with patch( + "petstore_api.api_client.ApiClient.call_api", return_value=mock_resp + ): + returned = self.fake_api.fake_return_float() + self.assertEqual(3.4, returned) + + def testTopLevelBoolJson(self): + """Test TopLevelBoolJson""" + mock_resp = Mock() + mock_resp.status = 200 + mock_resp.data = b'true' + mock_resp.getheaders.return_value = {} + mock_resp.getheader = ( + lambda name: "application/json" if name == "content-type" else Mock() + ) + with patch( + "petstore_api.api_client.ApiClient.call_api", return_value=mock_resp + ): + returned = self.fake_api.fake_return_boolean() + self.assertEqual(True, returned) + + def testTopLevelEnumJson(self): + """Test TopLevelEnumJson""" + mock_resp = Mock() + mock_resp.status = 200 + mock_resp.data = b'"a"' + mock_resp.getheaders.return_value = {} + mock_resp.getheader = ( + lambda name: "application/json" if name == "content-type" else Mock() + ) + with patch( + "petstore_api.api_client.ApiClient.call_api", return_value=mock_resp + ): + returned = self.fake_api.fake_return_enum() + self.assertEqual("a", returned)