Port middleware to Python 3

* Fix integer division: status_code/100 => status_code//100
* HTTP body type is bytes. Decode the HTTP body from UTF-8 to use it.
  Encode XML and JSON to UTF-8 to produce the HTTP body (for error
  messages).
* Factorize XML/JSON code a little bit more (ex: add the new
  content_type variable)
* Fix test_app on Python 3: decode HTTP from UTF-8 to load JSON
* tox.ini: add api.v2.test_app to Python 3.4

Change-Id: I4f916cba36306f776b01df915c55e314ade6b6ba
This commit is contained in:
Victor Stinner 2015-06-12 10:28:49 +02:00
parent 639a24dc95
commit 5977295daf
3 changed files with 36 additions and 15 deletions

View File

@ -23,6 +23,7 @@ import json
from lxml import etree from lxml import etree
from oslo_log import log from oslo_log import log
import six
import webob import webob
from ceilometer.api import hooks from ceilometer.api import hooks
@ -79,7 +80,7 @@ class ParsableErrorMiddleware(object):
return start_response(status, headers, exc_info) return start_response(status, headers, exc_info)
app_iter = self.app(environ, replacement_start_response) app_iter = self.app(environ, replacement_start_response)
if (state['status_code'] / 100) not in (2, 3): if (state['status_code'] // 100) not in (2, 3):
req = webob.Request(environ) req = webob.Request(environ)
# Find the first TranslationHook in the array of hooks and use the # Find the first TranslationHook in the array of hooks and use the
# translatable_error object from it # translatable_error object from it
@ -91,32 +92,44 @@ class ParsableErrorMiddleware(object):
user_locale = self.best_match_language(req.accept_language) user_locale = self.best_match_language(req.accept_language)
if (req.accept.best_match(['application/json', 'application/xml']) if (req.accept.best_match(['application/json', 'application/xml'])
== 'application/xml'): == 'application/xml'):
content_type = 'application/xml'
try: try:
# simple check xml is valid # simple check xml is valid
fault = etree.fromstring('\n'.join(app_iter)) fault = etree.fromstring(b'\n'.join(app_iter))
# Add the translated error to the xml data # Add the translated error to the xml data
if error is not None: if error is not None:
for fault_string in fault.findall('faultstring'): for fault_string in fault.findall('faultstring'):
fault_string.text = i18n.translate(error, fault_string.text = i18n.translate(error,
user_locale) user_locale)
body = ['<error_message>' + etree.tostring(fault) error_message = etree.tostring(fault)
+ '</error_message>'] body = b''.join((b'<error_message>',
error_message,
b'</error_message>'))
except etree.XMLSyntaxError as err: except etree.XMLSyntaxError as err:
LOG.error(_('Error parsing HTTP response: %s') % err) LOG.error(_('Error parsing HTTP response: %s'), err)
body = ['<error_message>%s' % state['status_code'] error_message = state['status_code']
+ '</error_message>'] body = '<error_message>%s</error_message>' % error_message
state['headers'].append(('Content-Type', 'application/xml')) if six.PY3:
body = body.encode('utf-8')
else: else:
content_type = 'application/json'
app_data = b'\n'.join(app_iter)
if six.PY3:
app_data = app_data.decode('utf-8')
try: try:
fault = json.loads('\n'.join(app_iter)) fault = json.loads(app_data)
if error is not None and 'faultstring' in fault: if error is not None and 'faultstring' in fault:
fault['faultstring'] = i18n.translate(error, fault['faultstring'] = i18n.translate(error,
user_locale) user_locale)
body = [json.dumps({'error_message': fault})]
except ValueError as err: except ValueError as err:
body = [json.dumps({'error_message': '\n'.join(app_iter)})] fault = app_data
state['headers'].append(('Content-Type', 'application/json')) body = json.dumps({'error_message': fault})
state['headers'].append(('Content-Length', str(len(body[0])))) if six.PY3:
body = body.encode('utf-8')
state['headers'].append(('Content-Length', str(len(body))))
state['headers'].append(('Content-Type', content_type))
body = [body]
else: else:
body = app_iter body = app_iter
return body return body

View File

@ -18,6 +18,7 @@
import json import json
import mock import mock
import six
import wsme import wsme
from ceilometer import i18n from ceilometer import i18n
@ -159,8 +160,11 @@ class TestApiMiddleware(v2.FunctionalTest):
def test_translated_then_untranslated_error(self): def test_translated_then_untranslated_error(self):
resp = self.get_json('/alarms/alarm-id-3', expect_errors=True) resp = self.get_json('/alarms/alarm-id-3', expect_errors=True)
self.assertEqual(404, resp.status_code) self.assertEqual(404, resp.status_code)
body = resp.body
if six.PY3:
body = body.decode('utf-8')
self.assertEqual("Alarm alarm-id-3 not found", self.assertEqual("Alarm alarm-id-3 not found",
json.loads(resp.body)['error_message'] json.loads(body)['error_message']
['faultstring']) ['faultstring'])
with mock.patch('ceilometer.api.controllers.' with mock.patch('ceilometer.api.controllers.'
@ -170,6 +174,9 @@ class TestApiMiddleware(v2.FunctionalTest):
resp = self.get_json('/alarms/alarm-id-5', expect_errors=True) resp = self.get_json('/alarms/alarm-id-5', expect_errors=True)
self.assertEqual(404, resp.status_code) self.assertEqual(404, resp.status_code)
body = resp.body
if six.PY3:
body = body.decode('utf-8')
self.assertEqual("untranslated_error", self.assertEqual("untranslated_error",
json.loads(resp.body)['error_message'] json.loads(body)['error_message']
['faultstring']) ['faultstring'])

View File

@ -47,6 +47,7 @@ commands =
deps = -r{toxinidir}/requirements.txt deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements-py3.txt -r{toxinidir}/test-requirements-py3.txt
commands = python -m testtools.run \ commands = python -m testtools.run \
ceilometer.tests.api.v2.test_app \
ceilometer.tests.api.v2.test_query \ ceilometer.tests.api.v2.test_query \
ceilometer.tests.compute.virt.libvirt.test_inspector \ ceilometer.tests.compute.virt.libvirt.test_inspector \
ceilometer.tests.compute.virt.vmware.test_vsphere_operations \ ceilometer.tests.compute.virt.vmware.test_vsphere_operations \