[Python] Fix Python UTF-8 Encoding Issue (#5679)

* Try decoding but don't bail on error

* Switch binary and ByteArray to bytes

* Read content type and parse appropriately

* Remove response parsing

* Remove response parsing and just return the data

* Update petshop examples w/ new generator code

* Fix copy/paste error with naming

* Update petstore examples

* Move response decoding to inside _preload_content block

* Update the clients again

* Use a raw string for the regex pattern

* Regenerate petstore clients

* Add bytes to python primitives as it's supported in 2.7 and 3

* Add bytes to the exports from model_utils

* Import bytes from model_utils

* Add conditional typing for regex pattern to match variable type

* Regenerate petstore clients

* Use read() instead of text() for asyncio

* Regenerate petstore clients

* Remove unused six import

* Regenerate petstore clients

* Add newline to kick Circle to re-run

* Remove whitespace from tox.ini

* Update more examples after ensure_updated

* Add sample updates that didn't run with the --batch flag

* Remove extra bracket in regex to remove warning

* Stop printing debug messages

* Add bytes examples to python doc generators

* Update generated FakeApi docs

* Regenerate api_client.py

* Remove print statements from generated clients

* Update bytes example in FakeApi.md. Again. I swear.

* Add yet another seemingly missing doc update

* Catch the error, decode the body, and re-throw

* Remove the updates now that the change is non-breaking

* Regenerate client

* Add bytes deserialization test

* Update exception parsing

* Add exception parsing for python-experimental

* Regenerate client with minor changes

* Revert test changes

* Regenerate model_utils.py

* Update confusing test name

* Remove bytes from mapping and examples

* Add back in the old binary/ByteArray to str mapping

* Update docs and api_client template

* Add experimental api_client changes

* Regenerate samples again

* Add Tornado handling to early return

* Try fixing Tornado python returns

* More documentation changes

* Re-generate the client code

* Remove bytes from test_format_test

* Remove more leftover bytes usages

* Switch bytes validation back to string

* Fix format_test template and regenerate

* Remove unused bytes var

* Remove bytes import from models and regenerate

* Remove bytes import from test_deserialization

* Reduce nested ifs

* Remove byte logic for now

* Regenerate client after latest changes

* Remove another bytes usage

* Regenerate after removing dangling byte string usage

* Reduce the scope of the try/catch in api_client

* Regenerate after try/catch cleanup

* Swap catch for except

* Regenerate Python client after api_client change

* Fix lint error on the generated api_client

* Add binary format test back in w/ string

* Add decoding to python-experimental and regenerate

* Import re into python-experimental api_client

* Ensure file upload json response is utf-8 encoded bytes
This commit is contained in:
Justin Niessner 2020-04-26 16:33:42 -04:00 committed by GitHub
parent cef5470ea8
commit db5941379f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 319 additions and 175 deletions

View File

@ -31,6 +31,7 @@ sidebar_label: python-experimental
<ul class="column-ul">
<li>bool</li>
<li>bytes</li>
<li>date</li>
<li>datetime</li>
<li>dict</li>

View File

@ -31,6 +31,7 @@ sidebar_label: python
<ul class="column-ul">
<li>bool</li>
<li>bytes</li>
<li>date</li>
<li>datetime</li>
<li>dict</li>

View File

@ -119,6 +119,7 @@ public class PythonClientCodegen extends DefaultCodegen implements CodegenConfig
languageSpecificPrimitives.add("object");
// TODO file and binary is mapped as `file`
languageSpecificPrimitives.add("file");
languageSpecificPrimitives.add("bytes");
typeMapping.clear();
typeMapping.put("integer", "int");
@ -828,7 +829,7 @@ public class PythonClientCodegen extends DefaultCodegen implements CodegenConfig
if (schema.getDiscriminator()!=null) {
toExclude = schema.getDiscriminator().getPropertyName();
}
example = packageName + ".models." + underscore(schema.getTitle())+"."+schema.getTitle()+"(";
// if required only:

View File

@ -884,8 +884,8 @@ public class PythonClientExperimentalCodegen extends PythonClientCodegen {
* Return a string representation of the Python types for the specified schema.
* Primitive types in the OAS specification are implemented in Python using the corresponding
* Python primitive types.
* Composed types (e.g. allAll, oneOf, anyOf) are represented in Python using list of types.
*
* Composed types (e.g. allAll, oneOf, anyOf) are represented in Python using list of types.
*
* @param p The OAS schema.
* @param prefix prepended to the returned value.
* @param suffix appended to the returned value.

View File

@ -22,7 +22,7 @@ import tornado.gen
from {{packageName}}.configuration import Configuration
import {{modelPackage}}
from {{packageName}} import rest
from {{packageName}}.exceptions import ApiValueError
from {{packageName}}.exceptions import ApiValueError, ApiException
class ApiClient(object):
@ -186,22 +186,43 @@ class ApiClient(object):
# use server/host defined in path or operation instead
url = _host + resource_path
# perform request and return response
response_data = {{#asyncio}}await {{/asyncio}}{{#tornado}}yield {{/tornado}}self.request(
method, url, query_params=query_params, headers=header_params,
post_params=post_params, body=body,
_preload_content=_preload_content,
_request_timeout=_request_timeout)
try:
# perform request and return response
response_data = {{#asyncio}}await {{/asyncio}}{{#tornado}}yield {{/tornado}}self.request(
method, url, query_params=query_params, headers=header_params,
post_params=post_params, body=body,
_preload_content=_preload_content,
_request_timeout=_request_timeout)
except ApiException as e:
e.body = e.body.decode('utf-8') if six.PY3 else e.body
raise e
content_type = response_data.getheader('content-type')
self.last_response = response_data
return_data = response_data
if _preload_content:
# deserialize response data
if response_type:
return_data = self.deserialize(response_data, response_type)
else:
return_data = None
if not _preload_content:
{{^tornado}}
return return_data
{{/tornado}}
{{#tornado}}
raise tornado.gen.Return(return_data)
{{/tornado}}
if six.PY3 and response_type not in ["file", "bytes"]:
match = None
if content_type is not None:
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s\;]?", content_type)
encoding = match.group(1) if match else "utf-8"
response_data.data = response_data.data.decode(encoding)
# deserialize response data
if response_type:
return_data = self.deserialize(response_data, response_type)
else:
return_data = None
{{^tornado}}
if _return_http_data_only:

View File

@ -166,7 +166,7 @@ class RESTClientObject(object):
r = await self.pool_manager.request(**args)
if _preload_content:
data = await r.text()
data = await r.read()
r = RESTResponse(r, data)
# log response body

View File

@ -7,6 +7,7 @@ import atexit
import mimetypes
from multiprocessing.pool import ThreadPool
import os
import re
# python 2 and python 3 compatibility library
import six
@ -17,7 +18,7 @@ import tornado.gen
from {{packageName}} import rest
from {{packageName}}.configuration import Configuration
from {{packageName}}.exceptions import ApiValueError
from {{packageName}}.exceptions import ApiValueError, ApiException
from {{packageName}}.model_utils import (
ModelNormal,
ModelSimple,
@ -176,26 +177,48 @@ class ApiClient(object):
# use server/host defined in path or operation instead
url = _host + resource_path
# perform request and return response
response_data = {{#asyncio}}await {{/asyncio}}{{#tornado}}yield {{/tornado}}self.request(
method, url, query_params=query_params, headers=header_params,
post_params=post_params, body=body,
_preload_content=_preload_content,
_request_timeout=_request_timeout)
try:
# perform request and return response
response_data = {{#asyncio}}await {{/asyncio}}{{#tornado}}yield {{/tornado}}self.request(
method, url, query_params=query_params, headers=header_params,
post_params=post_params, body=body,
_preload_content=_preload_content,
_request_timeout=_request_timeout)
except ApiException as e:
e.body = e.body.decode('utf-8') if six.PY3 else e.body
raise e
content_type = response_data.getheader('content-type')
self.last_response = response_data
return_data = response_data
if _preload_content:
# deserialize response data
if response_type:
return_data = self.deserialize(
response_data,
response_type,
_check_type
)
else:
return_data = None
if not _preload_content:
{{^tornado}}
return (return_data)
{{/tornado}}
{{#tornado}}
raise tornado.gen.Return(return_data)
{{/tornado}}
return return_data
if six.PY3 and response_type not in ["file", "bytes"]:
match = None
if content_type is not None:
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s\;]?", content_type)
encoding = match.group(1) if match else "utf-8"
response_data.data = response_data.data.decode(encoding)
# deserialize response data
if response_type:
return_data = self.deserialize(
response_data,
response_type,
_check_type
)
else:
return_data = None
{{^tornado}}
if _return_http_data_only:

View File

@ -209,11 +209,6 @@ class RESTClientObject(object):
if _preload_content:
r = RESTResponse(r)
# In the python 3, the response.data is bytes.
# we need to decode it to string.
if six.PY3:
r.data = r.data.decode('utf8')
# log response body
logger.debug("response body: %s", r.data)

View File

@ -8,7 +8,6 @@ import logging
import re
# python 2 and python 3 compatibility library
import six
from six.moves.urllib.parse import urlencode
import tornado
import tornado.gen
@ -28,11 +27,7 @@ class RESTResponse(io.IOBase):
self.reason = resp.reason
if resp.body:
# In Python 3, the response body is utf-8 encoded bytes.
if six.PY3:
self.data = resp.body.decode('utf-8')
else:
self.data = resp.body
self.data = resp.body
else:
self.data = None

View File

@ -27,7 +27,7 @@ from six.moves.urllib.parse import quote
from petstore_api.configuration import Configuration
import petstore_api.models
from petstore_api import rest
from petstore_api.exceptions import ApiValueError
from petstore_api.exceptions import ApiValueError, ApiException
class ApiClient(object):
@ -177,22 +177,38 @@ class ApiClient(object):
# use server/host defined in path or operation instead
url = _host + resource_path
# perform request and return response
response_data = await self.request(
method, url, query_params=query_params, headers=header_params,
post_params=post_params, body=body,
_preload_content=_preload_content,
_request_timeout=_request_timeout)
try:
# perform request and return response
response_data = await self.request(
method, url, query_params=query_params, headers=header_params,
post_params=post_params, body=body,
_preload_content=_preload_content,
_request_timeout=_request_timeout)
except ApiException as e:
e.body = e.body.decode('utf-8') if six.PY3 else e.body
raise e
content_type = response_data.getheader('content-type')
self.last_response = response_data
return_data = response_data
if _preload_content:
# deserialize response data
if response_type:
return_data = self.deserialize(response_data, response_type)
else:
return_data = None
if not _preload_content:
return return_data
if six.PY3 and response_type not in ["file", "bytes"]:
match = None
if content_type is not None:
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s\;]?", content_type)
encoding = match.group(1) if match else "utf-8"
response_data.data = response_data.data.decode(encoding)
# deserialize response data
if response_type:
return_data = self.deserialize(response_data, response_type)
else:
return_data = None
if _return_http_data_only:
return (return_data)

View File

@ -174,7 +174,7 @@ class RESTClientObject(object):
r = await self.pool_manager.request(**args)
if _preload_content:
data = await r.text()
data = await r.read()
r = RESTResponse(r, data)
# log response body

View File

@ -15,6 +15,7 @@ import atexit
import mimetypes
from multiprocessing.pool import ThreadPool
import os
import re
# python 2 and python 3 compatibility library
import six
@ -22,7 +23,7 @@ from six.moves.urllib.parse import quote
from petstore_api import rest
from petstore_api.configuration import Configuration
from petstore_api.exceptions import ApiValueError
from petstore_api.exceptions import ApiValueError, ApiException
from petstore_api.model_utils import (
ModelNormal,
ModelSimple,
@ -178,26 +179,43 @@ class ApiClient(object):
# use server/host defined in path or operation instead
url = _host + resource_path
# perform request and return response
response_data = self.request(
method, url, query_params=query_params, headers=header_params,
post_params=post_params, body=body,
_preload_content=_preload_content,
_request_timeout=_request_timeout)
try:
# perform request and return response
response_data = self.request(
method, url, query_params=query_params, headers=header_params,
post_params=post_params, body=body,
_preload_content=_preload_content,
_request_timeout=_request_timeout)
except ApiException as e:
e.body = e.body.decode('utf-8') if six.PY3 else e.body
raise e
content_type = response_data.getheader('content-type')
self.last_response = response_data
return_data = response_data
if _preload_content:
# deserialize response data
if response_type:
return_data = self.deserialize(
response_data,
response_type,
_check_type
)
else:
return_data = None
if not _preload_content:
return (return_data)
return return_data
if six.PY3 and response_type not in ["file", "bytes"]:
match = None
if content_type is not None:
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s\;]?", content_type)
encoding = match.group(1) if match else "utf-8"
response_data.data = response_data.data.decode(encoding)
# deserialize response data
if response_type:
return_data = self.deserialize(
response_data,
response_type,
_check_type
)
else:
return_data = None
if _return_http_data_only:
return (return_data)

View File

@ -217,11 +217,6 @@ class RESTClientObject(object):
if _preload_content:
r = RESTResponse(r)
# In the python 3, the response.data is bytes.
# we need to decode it to string.
if six.PY3:
r.data = r.data.decode('utf8')
# log response body
logger.debug("response body: %s", r.data)

View File

@ -150,4 +150,4 @@ class TestFormatTest(unittest.TestCase):
if __name__ == '__main__':
unittest.main()
unittest.main()

View File

@ -10,6 +10,7 @@ $ nosetests -v
"""
import os
import six
import sys
import unittest

View File

@ -377,6 +377,36 @@ class DeserializationTests(unittest.TestCase):
finally:
os.unlink(file_path)
def test_deserialize_binary_to_str(self):
"""Ensures that bytes deserialization works"""
response_types_mixed = (str,)
# sample from http://www.jtricks.com/download-text
HTTPResponse = namedtuple(
'urllib3_response_HTTPResponse',
['status', 'reason', 'data', 'getheaders', 'getheader']
)
headers = {}
def get_headers():
return headers
def get_header(name, default=None):
return headers.get(name, default)
data = "str"
http_response = HTTPResponse(
status=200,
reason='OK',
data=json.dumps(data).encode("utf-8") if six.PY3 else json.dumps(data),
getheaders=get_headers,
getheader=get_header
)
mock_response = RESTResponse(http_response)
result = self.deserialize(mock_response, response_types_mixed, True)
self.assertEqual(isinstance(result, str), True)
self.assertEqual(result, data)
def test_deserialize_string_boolean_map(self):
"""
Ensures that string boolean (additional properties)

View File

@ -329,7 +329,7 @@ class PetApiTests(unittest.TestCase):
http_response = HTTPResponse(
status=200,
reason='OK',
data=json.dumps(api_respponse),
data=json.dumps(api_respponse).encode('utf-8'),
getheaders=get_headers,
getheader=get_header
)

View File

@ -28,7 +28,7 @@ import tornado.gen
from petstore_api.configuration import Configuration
import petstore_api.models
from petstore_api import rest
from petstore_api.exceptions import ApiValueError
from petstore_api.exceptions import ApiValueError, ApiException
class ApiClient(object):
@ -178,22 +178,38 @@ class ApiClient(object):
# use server/host defined in path or operation instead
url = _host + resource_path
# perform request and return response
response_data = yield self.request(
method, url, query_params=query_params, headers=header_params,
post_params=post_params, body=body,
_preload_content=_preload_content,
_request_timeout=_request_timeout)
try:
# perform request and return response
response_data = yield self.request(
method, url, query_params=query_params, headers=header_params,
post_params=post_params, body=body,
_preload_content=_preload_content,
_request_timeout=_request_timeout)
except ApiException as e:
e.body = e.body.decode('utf-8') if six.PY3 else e.body
raise e
content_type = response_data.getheader('content-type')
self.last_response = response_data
return_data = response_data
if _preload_content:
# deserialize response data
if response_type:
return_data = self.deserialize(response_data, response_type)
else:
return_data = None
if not _preload_content:
raise tornado.gen.Return(return_data)
if six.PY3 and response_type not in ["file", "bytes"]:
match = None
if content_type is not None:
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s\;]?", content_type)
encoding = match.group(1) if match else "utf-8"
response_data.data = response_data.data.decode(encoding)
# deserialize response data
if response_type:
return_data = self.deserialize(response_data, response_type)
else:
return_data = None
if _return_http_data_only:
raise tornado.gen.Return(return_data)

View File

@ -16,7 +16,6 @@ import logging
import re
# python 2 and python 3 compatibility library
import six
from six.moves.urllib.parse import urlencode
import tornado
import tornado.gen
@ -36,11 +35,7 @@ class RESTResponse(io.IOBase):
self.reason = resp.reason
if resp.body:
# In Python 3, the response body is utf-8 encoded bytes.
if six.PY3:
self.data = resp.body.decode('utf-8')
else:
self.data = resp.body
self.data = resp.body
else:
self.data = None

View File

@ -27,7 +27,7 @@ from six.moves.urllib.parse import quote
from petstore_api.configuration import Configuration
import petstore_api.models
from petstore_api import rest
from petstore_api.exceptions import ApiValueError
from petstore_api.exceptions import ApiValueError, ApiException
class ApiClient(object):
@ -176,22 +176,38 @@ class ApiClient(object):
# use server/host defined in path or operation instead
url = _host + resource_path
# perform request and return response
response_data = self.request(
method, url, query_params=query_params, headers=header_params,
post_params=post_params, body=body,
_preload_content=_preload_content,
_request_timeout=_request_timeout)
try:
# perform request and return response
response_data = self.request(
method, url, query_params=query_params, headers=header_params,
post_params=post_params, body=body,
_preload_content=_preload_content,
_request_timeout=_request_timeout)
except ApiException as e:
e.body = e.body.decode('utf-8') if six.PY3 else e.body
raise e
content_type = response_data.getheader('content-type')
self.last_response = response_data
return_data = response_data
if _preload_content:
# deserialize response data
if response_type:
return_data = self.deserialize(response_data, response_type)
else:
return_data = None
if not _preload_content:
return return_data
if six.PY3 and response_type not in ["file", "bytes"]:
match = None
if content_type is not None:
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s\;]?", content_type)
encoding = match.group(1) if match else "utf-8"
response_data.data = response_data.data.decode(encoding)
# deserialize response data
if response_type:
return_data = self.deserialize(response_data, response_type)
else:
return_data = None
if _return_http_data_only:
return (return_data)

View File

@ -217,11 +217,6 @@ class RESTClientObject(object):
if _preload_content:
r = RESTResponse(r)
# In the python 3, the response.data is bytes.
# we need to decode it to string.
if six.PY3:
r.data = r.data.decode('utf8')
# log response body
logger.debug("response body: %s", r.data)

View File

@ -36,19 +36,19 @@ class TestFormatTest(unittest.TestCase):
# model = petstore_api.models.format_test.FormatTest() # noqa: E501
if include_optional :
return FormatTest(
integer = 1E+1,
int32 = 2E+1,
int64 = 56,
number = 32.1,
float = 54.3,
double = 67.8,
string = 'a',
byte = 'YQ==',
binary = bytes(b'blah'),
date = datetime.datetime.strptime('1975-12-30', '%Y-%m-%d').date(),
date_time = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'),
uuid = '72f98069-206d-4f12-9f12-3d1e525a8e84',
password = '0123456789',
integer = 1E+1,
int32 = 2E+1,
int64 = 56,
number = 32.1,
float = 54.3,
double = 67.8,
string = 'a',
byte = 'YQ==',
binary = 'bytes',
date = datetime.datetime.strptime('1975-12-30', '%Y-%m-%d').date(),
date_time = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'),
uuid = '72f98069-206d-4f12-9f12-3d1e525a8e84',
password = '0123456789',
big_decimal = 1
)
else :

View File

@ -10,6 +10,7 @@ $ nosetests -v
"""
import os
import six
import sys
import unittest

View File

@ -15,6 +15,7 @@ import atexit
import mimetypes
from multiprocessing.pool import ThreadPool
import os
import re
# python 2 and python 3 compatibility library
import six
@ -22,7 +23,7 @@ from six.moves.urllib.parse import quote
from petstore_api import rest
from petstore_api.configuration import Configuration
from petstore_api.exceptions import ApiValueError
from petstore_api.exceptions import ApiValueError, ApiException
from petstore_api.model_utils import (
ModelNormal,
ModelSimple,
@ -178,26 +179,43 @@ class ApiClient(object):
# use server/host defined in path or operation instead
url = _host + resource_path
# perform request and return response
response_data = self.request(
method, url, query_params=query_params, headers=header_params,
post_params=post_params, body=body,
_preload_content=_preload_content,
_request_timeout=_request_timeout)
try:
# perform request and return response
response_data = self.request(
method, url, query_params=query_params, headers=header_params,
post_params=post_params, body=body,
_preload_content=_preload_content,
_request_timeout=_request_timeout)
except ApiException as e:
e.body = e.body.decode('utf-8') if six.PY3 else e.body
raise e
content_type = response_data.getheader('content-type')
self.last_response = response_data
return_data = response_data
if _preload_content:
# deserialize response data
if response_type:
return_data = self.deserialize(
response_data,
response_type,
_check_type
)
else:
return_data = None
if not _preload_content:
return (return_data)
return return_data
if six.PY3 and response_type not in ["file", "bytes"]:
match = None
if content_type is not None:
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s\;]?", content_type)
encoding = match.group(1) if match else "utf-8"
response_data.data = response_data.data.decode(encoding)
# deserialize response data
if response_type:
return_data = self.deserialize(
response_data,
response_type,
_check_type
)
else:
return_data = None
if _return_http_data_only:
return (return_data)

View File

@ -217,11 +217,6 @@ class RESTClientObject(object):
if _preload_content:
r = RESTResponse(r)
# In the python 3, the response.data is bytes.
# we need to decode it to string.
if six.PY3:
r.data = r.data.decode('utf8')
# log response body
logger.debug("response body: %s", r.data)

View File

@ -27,7 +27,7 @@ from six.moves.urllib.parse import quote
from petstore_api.configuration import Configuration
import petstore_api.models
from petstore_api import rest
from petstore_api.exceptions import ApiValueError
from petstore_api.exceptions import ApiValueError, ApiException
class ApiClient(object):
@ -176,22 +176,38 @@ class ApiClient(object):
# use server/host defined in path or operation instead
url = _host + resource_path
# perform request and return response
response_data = self.request(
method, url, query_params=query_params, headers=header_params,
post_params=post_params, body=body,
_preload_content=_preload_content,
_request_timeout=_request_timeout)
try:
# perform request and return response
response_data = self.request(
method, url, query_params=query_params, headers=header_params,
post_params=post_params, body=body,
_preload_content=_preload_content,
_request_timeout=_request_timeout)
except ApiException as e:
e.body = e.body.decode('utf-8') if six.PY3 else e.body
raise e
content_type = response_data.getheader('content-type')
self.last_response = response_data
return_data = response_data
if _preload_content:
# deserialize response data
if response_type:
return_data = self.deserialize(response_data, response_type)
else:
return_data = None
if not _preload_content:
return return_data
if six.PY3 and response_type not in ["file", "bytes"]:
match = None
if content_type is not None:
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s\;]?", content_type)
encoding = match.group(1) if match else "utf-8"
response_data.data = response_data.data.decode(encoding)
# deserialize response data
if response_type:
return_data = self.deserialize(response_data, response_type)
else:
return_data = None
if _return_http_data_only:
return (return_data)

View File

@ -217,11 +217,6 @@ class RESTClientObject(object):
if _preload_content:
r = RESTResponse(r)
# In the python 3, the response.data is bytes.
# we need to decode it to string.
if six.PY3:
r.data = r.data.decode('utf8')
# log response body
logger.debug("response body: %s", r.data)

View File

@ -30,8 +30,8 @@ class EnumClass implements ModelInterface
private const MODEL_SCHEMA = <<<'SCHEMA'
{
"type" : "string",
"default" : "-efg",
"enum" : [ "_abc", "-efg", "(xyz)" ]
"enum" : [ "_abc", "-efg", "(xyz)" ],
"default" : "-efg"
}
SCHEMA;