Skip to content

Commit

Permalink
[Python] Fix Python UTF-8 Encoding Issue (#5679)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
CrshOverride committed Apr 26, 2020
1 parent cef5470 commit db59413
Show file tree
Hide file tree
Showing 28 changed files with 319 additions and 175 deletions.
1 change: 1 addition & 0 deletions docs/generators/python-experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
1 change: 1 addition & 0 deletions docs/generators/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ public PythonClientCodegen() {
languageSpecificPrimitives.add("object");
// TODO file and binary is mapped as `file`
languageSpecificPrimitives.add("file");
languageSpecificPrimitives.add("bytes");

typeMapping.clear();
typeMapping.put("integer", "int");
Expand Down Expand Up @@ -828,7 +829,7 @@ private String toExampleValueRecursive(Schema schema, List<String> included_sche
if (schema.getDiscriminator()!=null) {
toExclude = schema.getDiscriminator().getPropertyName();
}

example = packageName + ".models." + underscore(schema.getTitle())+"."+schema.getTitle()+"(";

// if required only:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -884,8 +884,8 @@ public String getSimpleTypeDeclaration(Schema schema) {
* 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
42 changes: 29 additions & 13 deletions samples/client/petstore/python-asyncio/petstore_api/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
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):
Expand Down Expand Up @@ -177,22 +177,38 @@ async def __call_api(
# 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ async def request(self, method, url, query_params=None, headers=None,
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
import mimetypes
from multiprocessing.pool import ThreadPool
import os
import re

# python 2 and python 3 compatibility library
import six
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,
Expand Down Expand Up @@ -178,26 +179,43 @@ def __call_api(
# 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,6 @@ def request(self, method, url, query_params=None, headers=None,
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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,4 @@ def test_string(self):


if __name__ == '__main__':
unittest.main()
unittest.main()
Loading

0 comments on commit db59413

Please sign in to comment.