Skip to content

Commit db59413

Browse files
authored
[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
1 parent cef5470 commit db59413

File tree

28 files changed

+319
-175
lines changed

28 files changed

+319
-175
lines changed

docs/generators/python-experimental.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ sidebar_label: python-experimental
3131

3232
<ul class="column-ul">
3333
<li>bool</li>
34+
<li>bytes</li>
3435
<li>date</li>
3536
<li>datetime</li>
3637
<li>dict</li>

docs/generators/python.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ sidebar_label: python
3131

3232
<ul class="column-ul">
3333
<li>bool</li>
34+
<li>bytes</li>
3435
<li>date</li>
3536
<li>datetime</li>
3637
<li>dict</li>

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientCodegen.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ public PythonClientCodegen() {
119119
languageSpecificPrimitives.add("object");
120120
// TODO file and binary is mapped as `file`
121121
languageSpecificPrimitives.add("file");
122+
languageSpecificPrimitives.add("bytes");
122123

123124
typeMapping.clear();
124125
typeMapping.put("integer", "int");
@@ -828,7 +829,7 @@ private String toExampleValueRecursive(Schema schema, List<String> included_sche
828829
if (schema.getDiscriminator()!=null) {
829830
toExclude = schema.getDiscriminator().getPropertyName();
830831
}
831-
832+
832833
example = packageName + ".models." + underscore(schema.getTitle())+"."+schema.getTitle()+"(";
833834

834835
// if required only:

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonClientExperimentalCodegen.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -884,8 +884,8 @@ public String getSimpleTypeDeclaration(Schema schema) {
884884
* Return a string representation of the Python types for the specified schema.
885885
* Primitive types in the OAS specification are implemented in Python using the corresponding
886886
* Python primitive types.
887-
* Composed types (e.g. allAll, oneOf, anyOf) are represented in Python using list of types.
888-
*
887+
* Composed types (e.g. allAll, oneOf, anyOf) are represented in Python using list of types.
888+
*
889889
* @param p The OAS schema.
890890
* @param prefix prepended to the returned value.
891891
* @param suffix appended to the returned value.

modules/openapi-generator/src/main/resources/python/api_client.mustache

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import tornado.gen
2222
from {{packageName}}.configuration import Configuration
2323
import {{modelPackage}}
2424
from {{packageName}} import rest
25-
from {{packageName}}.exceptions import ApiValueError
25+
from {{packageName}}.exceptions import ApiValueError, ApiException
2626

2727

2828
class ApiClient(object):
@@ -186,22 +186,43 @@ class ApiClient(object):
186186
# use server/host defined in path or operation instead
187187
url = _host + resource_path
188188

189-
# perform request and return response
190-
response_data = {{#asyncio}}await {{/asyncio}}{{#tornado}}yield {{/tornado}}self.request(
191-
method, url, query_params=query_params, headers=header_params,
192-
post_params=post_params, body=body,
193-
_preload_content=_preload_content,
194-
_request_timeout=_request_timeout)
189+
try:
190+
# perform request and return response
191+
response_data = {{#asyncio}}await {{/asyncio}}{{#tornado}}yield {{/tornado}}self.request(
192+
method, url, query_params=query_params, headers=header_params,
193+
post_params=post_params, body=body,
194+
_preload_content=_preload_content,
195+
_request_timeout=_request_timeout)
196+
except ApiException as e:
197+
e.body = e.body.decode('utf-8') if six.PY3 else e.body
198+
raise e
199+
200+
content_type = response_data.getheader('content-type')
195201

196202
self.last_response = response_data
197203

198204
return_data = response_data
199-
if _preload_content:
200-
# deserialize response data
201-
if response_type:
202-
return_data = self.deserialize(response_data, response_type)
203-
else:
204-
return_data = None
205+
206+
if not _preload_content:
207+
{{^tornado}}
208+
return return_data
209+
{{/tornado}}
210+
{{#tornado}}
211+
raise tornado.gen.Return(return_data)
212+
{{/tornado}}
213+
214+
if six.PY3 and response_type not in ["file", "bytes"]:
215+
match = None
216+
if content_type is not None:
217+
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s\;]?", content_type)
218+
encoding = match.group(1) if match else "utf-8"
219+
response_data.data = response_data.data.decode(encoding)
220+
221+
# deserialize response data
222+
if response_type:
223+
return_data = self.deserialize(response_data, response_type)
224+
else:
225+
return_data = None
205226

206227
{{^tornado}}
207228
if _return_http_data_only:

modules/openapi-generator/src/main/resources/python/asyncio/rest.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ class RESTClientObject(object):
166166
r = await self.pool_manager.request(**args)
167167
if _preload_content:
168168

169-
data = await r.text()
169+
data = await r.read()
170170
r = RESTResponse(r, data)
171171

172172
# log response body

modules/openapi-generator/src/main/resources/python/python-experimental/api_client.mustache

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import atexit
77
import mimetypes
88
from multiprocessing.pool import ThreadPool
99
import os
10+
import re
1011

1112
# python 2 and python 3 compatibility library
1213
import six
@@ -17,7 +18,7 @@ import tornado.gen
1718

1819
from {{packageName}} import rest
1920
from {{packageName}}.configuration import Configuration
20-
from {{packageName}}.exceptions import ApiValueError
21+
from {{packageName}}.exceptions import ApiValueError, ApiException
2122
from {{packageName}}.model_utils import (
2223
ModelNormal,
2324
ModelSimple,
@@ -176,26 +177,48 @@ class ApiClient(object):
176177
# use server/host defined in path or operation instead
177178
url = _host + resource_path
178179

179-
# perform request and return response
180-
response_data = {{#asyncio}}await {{/asyncio}}{{#tornado}}yield {{/tornado}}self.request(
181-
method, url, query_params=query_params, headers=header_params,
182-
post_params=post_params, body=body,
183-
_preload_content=_preload_content,
184-
_request_timeout=_request_timeout)
180+
try:
181+
# perform request and return response
182+
response_data = {{#asyncio}}await {{/asyncio}}{{#tornado}}yield {{/tornado}}self.request(
183+
method, url, query_params=query_params, headers=header_params,
184+
post_params=post_params, body=body,
185+
_preload_content=_preload_content,
186+
_request_timeout=_request_timeout)
187+
except ApiException as e:
188+
e.body = e.body.decode('utf-8') if six.PY3 else e.body
189+
raise e
190+
191+
content_type = response_data.getheader('content-type')
185192

186193
self.last_response = response_data
187194

188195
return_data = response_data
189-
if _preload_content:
190-
# deserialize response data
191-
if response_type:
192-
return_data = self.deserialize(
193-
response_data,
194-
response_type,
195-
_check_type
196-
)
197-
else:
198-
return_data = None
196+
197+
if not _preload_content:
198+
{{^tornado}}
199+
return (return_data)
200+
{{/tornado}}
201+
{{#tornado}}
202+
raise tornado.gen.Return(return_data)
203+
{{/tornado}}
204+
return return_data
205+
206+
if six.PY3 and response_type not in ["file", "bytes"]:
207+
match = None
208+
if content_type is not None:
209+
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s\;]?", content_type)
210+
encoding = match.group(1) if match else "utf-8"
211+
response_data.data = response_data.data.decode(encoding)
212+
213+
# deserialize response data
214+
if response_type:
215+
return_data = self.deserialize(
216+
response_data,
217+
response_type,
218+
_check_type
219+
)
220+
else:
221+
return_data = None
199222

200223
{{^tornado}}
201224
if _return_http_data_only:

modules/openapi-generator/src/main/resources/python/rest.mustache

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,11 +209,6 @@ class RESTClientObject(object):
209209
if _preload_content:
210210
r = RESTResponse(r)
211211

212-
# In the python 3, the response.data is bytes.
213-
# we need to decode it to string.
214-
if six.PY3:
215-
r.data = r.data.decode('utf8')
216-
217212
# log response body
218213
logger.debug("response body: %s", r.data)
219214

modules/openapi-generator/src/main/resources/python/tornado/rest.mustache

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import logging
88
import re
99

1010
# python 2 and python 3 compatibility library
11-
import six
1211
from six.moves.urllib.parse import urlencode
1312
import tornado
1413
import tornado.gen
@@ -28,11 +27,7 @@ class RESTResponse(io.IOBase):
2827
self.reason = resp.reason
2928

3029
if resp.body:
31-
# In Python 3, the response body is utf-8 encoded bytes.
32-
if six.PY3:
33-
self.data = resp.body.decode('utf-8')
34-
else:
35-
self.data = resp.body
30+
self.data = resp.body
3631
else:
3732
self.data = None
3833

samples/client/petstore/python-asyncio/petstore_api/api_client.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from petstore_api.configuration import Configuration
2828
import petstore_api.models
2929
from petstore_api import rest
30-
from petstore_api.exceptions import ApiValueError
30+
from petstore_api.exceptions import ApiValueError, ApiException
3131

3232

3333
class ApiClient(object):
@@ -177,22 +177,38 @@ async def __call_api(
177177
# use server/host defined in path or operation instead
178178
url = _host + resource_path
179179

180-
# perform request and return response
181-
response_data = await self.request(
182-
method, url, query_params=query_params, headers=header_params,
183-
post_params=post_params, body=body,
184-
_preload_content=_preload_content,
185-
_request_timeout=_request_timeout)
180+
try:
181+
# perform request and return response
182+
response_data = await self.request(
183+
method, url, query_params=query_params, headers=header_params,
184+
post_params=post_params, body=body,
185+
_preload_content=_preload_content,
186+
_request_timeout=_request_timeout)
187+
except ApiException as e:
188+
e.body = e.body.decode('utf-8') if six.PY3 else e.body
189+
raise e
190+
191+
content_type = response_data.getheader('content-type')
186192

187193
self.last_response = response_data
188194

189195
return_data = response_data
190-
if _preload_content:
191-
# deserialize response data
192-
if response_type:
193-
return_data = self.deserialize(response_data, response_type)
194-
else:
195-
return_data = None
196+
197+
if not _preload_content:
198+
return return_data
199+
200+
if six.PY3 and response_type not in ["file", "bytes"]:
201+
match = None
202+
if content_type is not None:
203+
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s\;]?", content_type)
204+
encoding = match.group(1) if match else "utf-8"
205+
response_data.data = response_data.data.decode(encoding)
206+
207+
# deserialize response data
208+
if response_type:
209+
return_data = self.deserialize(response_data, response_type)
210+
else:
211+
return_data = None
196212

197213
if _return_http_data_only:
198214
return (return_data)

samples/client/petstore/python-asyncio/petstore_api/rest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ async def request(self, method, url, query_params=None, headers=None,
174174
r = await self.pool_manager.request(**args)
175175
if _preload_content:
176176

177-
data = await r.text()
177+
data = await r.read()
178178
r = RESTResponse(r, data)
179179

180180
# log response body

samples/client/petstore/python-experimental/petstore_api/api_client.py

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@
1515
import mimetypes
1616
from multiprocessing.pool import ThreadPool
1717
import os
18+
import re
1819

1920
# python 2 and python 3 compatibility library
2021
import six
2122
from six.moves.urllib.parse import quote
2223

2324
from petstore_api import rest
2425
from petstore_api.configuration import Configuration
25-
from petstore_api.exceptions import ApiValueError
26+
from petstore_api.exceptions import ApiValueError, ApiException
2627
from petstore_api.model_utils import (
2728
ModelNormal,
2829
ModelSimple,
@@ -178,26 +179,43 @@ def __call_api(
178179
# use server/host defined in path or operation instead
179180
url = _host + resource_path
180181

181-
# perform request and return response
182-
response_data = self.request(
183-
method, url, query_params=query_params, headers=header_params,
184-
post_params=post_params, body=body,
185-
_preload_content=_preload_content,
186-
_request_timeout=_request_timeout)
182+
try:
183+
# perform request and return response
184+
response_data = self.request(
185+
method, url, query_params=query_params, headers=header_params,
186+
post_params=post_params, body=body,
187+
_preload_content=_preload_content,
188+
_request_timeout=_request_timeout)
189+
except ApiException as e:
190+
e.body = e.body.decode('utf-8') if six.PY3 else e.body
191+
raise e
192+
193+
content_type = response_data.getheader('content-type')
187194

188195
self.last_response = response_data
189196

190197
return_data = response_data
191-
if _preload_content:
192-
# deserialize response data
193-
if response_type:
194-
return_data = self.deserialize(
195-
response_data,
196-
response_type,
197-
_check_type
198-
)
199-
else:
200-
return_data = None
198+
199+
if not _preload_content:
200+
return (return_data)
201+
return return_data
202+
203+
if six.PY3 and response_type not in ["file", "bytes"]:
204+
match = None
205+
if content_type is not None:
206+
match = re.search(r"charset=([a-zA-Z\-\d]+)[\s\;]?", content_type)
207+
encoding = match.group(1) if match else "utf-8"
208+
response_data.data = response_data.data.decode(encoding)
209+
210+
# deserialize response data
211+
if response_type:
212+
return_data = self.deserialize(
213+
response_data,
214+
response_type,
215+
_check_type
216+
)
217+
else:
218+
return_data = None
201219

202220
if _return_http_data_only:
203221
return (return_data)

samples/client/petstore/python-experimental/petstore_api/rest.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -217,11 +217,6 @@ def request(self, method, url, query_params=None, headers=None,
217217
if _preload_content:
218218
r = RESTResponse(r)
219219

220-
# In the python 3, the response.data is bytes.
221-
# we need to decode it to string.
222-
if six.PY3:
223-
r.data = r.data.decode('utf8')
224-
225220
# log response body
226221
logger.debug("response body: %s", r.data)
227222

samples/client/petstore/python-experimental/test/test_format_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,4 +150,4 @@ def test_string(self):
150150

151151

152152
if __name__ == '__main__':
153-
unittest.main()
153+
unittest.main()

0 commit comments

Comments
 (0)