Fix problems in unicode template support

If we use unicode as resource name or parameter,
we are likely to encounter unicode problems.
This patch aims to fix unicode problems, and
there are several things to be clear:

1. For the class wrap with six.python_2_unicode_compatible, in
python2 their original __str__ function will map to __unicode__,
and new __str__ function comes from __unicode__.encode.
So we should always return unicode in __str__() if wrap
with python_2_unicode_compatible.

2. python_2_unicode_compatible will not handle __repr__,
__repr__ should return str in all versions of python.

Co-Authored-By: Ethan Lynn <xjunlin@cn.ibm.com>
Closes-Bug: #1524194
Change-Id: Ib4af43d153e0edf9d66308bf8e7b93a3be501d2e
This commit is contained in:
Ethan Lynn 2016-02-17 16:32:51 +08:00 committed by Ethan Lynn
parent c87f69fa93
commit 6cf8dc6e56
8 changed files with 172 additions and 5 deletions

View File

@ -16,7 +16,10 @@
# It's based on oslo.i18n usage in OpenStack Keystone project and
# recommendations from http://docs.openstack.org/developer/oslo.i18n/usage.html
import six
import oslo_i18n as i18n
from oslo_utils import encodeutils
_translators = i18n.TranslatorFactory(domain='heat')
@ -33,3 +36,19 @@ _LI = _translators.log_info
_LW = _translators.log_warning
_LE = _translators.log_error
_LC = _translators.log_critical
def repr_wraper(klass):
"""A decorator that defines __repr__ method under Python 2.
Under Python 2 it will encode repr return value to str type.
Under Python 3 it does nothing.
"""
if six.PY2:
if '__repr__' not in klass.__dict__:
raise ValueError("@repr_wraper cannot be applied "
"to %s because it doesn't define __repr__()." %
klass.__name__)
klass._repr = klass.__repr__
klass.__repr__ = lambda self: encodeutils.safe_encode(self._repr())
return klass

View File

@ -18,6 +18,7 @@ import six
from heat.common.i18n import _
from heat.common.i18n import _LW
from heat.common.i18n import repr_wraper
from heat.engine import constraints as constr
from heat.engine import support
@ -123,6 +124,7 @@ class Attribute(object):
}
@repr_wraper
class Attributes(collections.Mapping):
"""Models a collection of Resource Attributes."""

View File

@ -18,12 +18,14 @@ import six
from heat.common import exception
from heat.common.i18n import _
from heat.common.i18n import repr_wraper
class CircularDependencyException(exception.HeatException):
msg_fmt = _("Circular Dependency Found: %(cycle)s")
@repr_wraper
@six.python_2_unicode_compatible
class Node(object):
"""A node in a dependency graph."""
@ -88,7 +90,7 @@ class Node(object):
def __str__(self):
"""Return a human-readable string representation of the node."""
text = '{%s}' % ', '.join(str(n) for n in self)
text = '{%s}' % ', '.join(six.text_type(n) for n in self)
return six.text_type(text)
def __repr__(self):
@ -143,7 +145,8 @@ class Graph(collections.defaultdict):
def __str__(self):
"""Convert the graph to a human-readable string."""
pairs = ('%s: %s' % (str(k), str(v)) for k, v in six.iteritems(self))
pairs = ('%s: %s' % (six.text_type(k), six.text_type(v))
for k, v in six.iteritems(self))
text = '{%s}' % ', '.join(pairs)
return six.text_type(text)
@ -165,6 +168,7 @@ class Graph(collections.defaultdict):
raise CircularDependencyException(cycle=six.text_type(graph))
@repr_wraper
@six.python_2_unicode_compatible
class Dependencies(object):
"""Helper class for calculating a dependency graph."""

View File

@ -19,6 +19,7 @@ import six
from heat.common import exception
from heat.common.i18n import _LW
from heat.common.i18n import repr_wraper
from heat.engine import function
from heat.engine import properties
@ -27,6 +28,7 @@ LOG = log.getLogger(__name__)
__all__ = ['ResourceDefinition']
@repr_wraper
class ResourceDefinitionCore(object):
"""A definition of a resource, independent of any template format."""

View File

@ -16,12 +16,14 @@ import types
import eventlet
from oslo_log import log as logging
from oslo_utils import encodeutils
from oslo_utils import excutils
import six
from six import reraise as raise_
from heat.common.i18n import _
from heat.common.i18n import _LI
from heat.common.i18n import repr_wraper
from heat.common import timeutils
LOG = logging.getLogger(__name__)
@ -40,10 +42,10 @@ def task_description(task):
if name is not None and isinstance(task, (types.MethodType,
types.FunctionType)):
if getattr(task, '__self__', None) is not None:
return '%s from %s' % (name, task.__self__)
return '%s from %s' % (six.text_type(name), task.__self__)
else:
return six.text_type(name)
return repr(task)
return encodeutils.safe_decode(repr(task))
class Timeout(BaseException):
@ -331,6 +333,7 @@ def wrappertask(task):
return wrapper
@repr_wraper
class DependencyTaskGroup(object):
"""Task which manages group of subtasks that have ordering dependencies."""
@ -369,7 +372,7 @@ class DependencyTaskGroup(object):
def __repr__(self):
"""Return a string representation of the task."""
text = '%s(%s)' % (type(self).__name__, self.name)
return six.text_type(text)
return text
def __call__(self):
"""Return a co-routine which runs the task group."""

View File

@ -16,6 +16,7 @@ import six
from heat.common import exception
from heat.common.i18n import _LI
from heat.common.i18n import repr_wraper
from heat.engine import dependencies
from heat.engine import scheduler
from heat.objects import resource as resource_objects
@ -23,6 +24,7 @@ from heat.objects import resource as resource_objects
LOG = logging.getLogger(__name__)
@repr_wraper
class StackUpdate(object):
"""A Task to perform the update of an existing stack to a new template."""

View File

@ -16,6 +16,7 @@ import contextlib
import eventlet
import six
from heat.common.i18n import repr_wraper
from heat.common import timeutils
from heat.engine import dependencies
from heat.engine import scheduler
@ -823,6 +824,27 @@ class DescriptionTest(common.HeatTestCase):
self.assertEqual('o', scheduler.task_description(C()))
def test_unicode(self):
@repr_wraper
@six.python_2_unicode_compatible
class C(object):
def __str__(self):
return u'C "\u2665"'
def __repr__(self):
return u'\u2665'
def __call__(self):
pass
def m(self):
pass
self.assertEqual(u'm from C "\u2665"',
scheduler.task_description(C().m))
self.assertEqual(u'\u2665',
scheduler.task_description(C()))
class WrapperTaskTest(common.HeatTestCase):

View File

@ -0,0 +1,113 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from heat_integrationtests.functional import functional_base
class StackUnicodeTemplateTest(functional_base.FunctionalTestsBase):
random_template = u'''
heat_template_version: 2014-10-16
description: \u8fd9\u662f\u4e00\u4e2a\u63cf\u8ff0
parameters:
\u53c2\u6570:
type: number
default: 10
label: \u6807\u7b7e
description: \u8fd9\u662f\u4e00\u4e2a\u63cf\u8ff0
resources:
\u8d44\u6e90:
type: OS::Heat::RandomString
properties:
length: {get_param: \u53c2\u6570}
outputs:
\u8f93\u51fa:
description: \u8fd9\u662f\u4e00\u4e2a\u63cf\u8ff0
value: {get_attr: [\u8d44\u6e90, value]}
'''
def setUp(self):
super(StackUnicodeTemplateTest, self).setUp()
def _assert_results(self, result):
self.assertTrue(result['disable_rollback'])
self.assertIsNone(result['parent'])
self.assertEqual(u'\u8fd9\u662f\u4e00\u4e2a\u63cf\u8ff0',
result['template_description'])
self.assertEqual(u'10', result['parameters'][u'\u53c2\u6570'])
def _assert_preview_results(self, result):
self._assert_results(result)
res = result['resources'][0]
self.assertEqual('/resources/%s' % res['resource_name'],
res['resource_identity']['path'])
def _assert_create_results(self, result):
self._assert_results(result)
output = result['outputs'][0]
self.assertEqual(u'\u8fd9\u662f\u4e00\u4e2a\u63cf\u8ff0',
output['description'])
self.assertEqual(u'\u8f93\u51fa', output['output_key'])
self.assertIsNotNone(output['output_value'])
def _assert_resource_results(self, result):
self.assertEqual(u'\u8d44\u6e90', result['resource_name'])
self.assertEqual('OS::Heat::RandomString',
result['resource_type'])
def test_template_validate_basic(self):
ret = self.client.stacks.validate(template=self.random_template)
expected = {
'Description': u'\u8fd9\u662f\u4e00\u4e2a\u63cf\u8ff0',
'Parameters': {
u'\u53c2\u6570': {
'Default': 10,
'Description': u'\u8fd9\u662f\u4e00\u4e2a\u63cf\u8ff0',
'Label': u'\u6807\u7b7e',
'NoEcho': 'false',
'Type': 'Number'}
}
}
self.assertEqual(expected, ret)
def test_template_validate_override_default(self):
env = {'parameters': {u'\u53c2\u6570': 5}}
ret = self.client.stacks.validate(template=self.random_template,
environment=env)
expected = {
'Description': u'\u8fd9\u662f\u4e00\u4e2a\u63cf\u8ff0',
'Parameters': {
u'\u53c2\u6570': {
'Default': 10,
'Value': 5,
'Description': u'\u8fd9\u662f\u4e00\u4e2a\u63cf\u8ff0',
'Label': u'\u6807\u7b7e',
'NoEcho': 'false',
'Type': 'Number'}
}
}
self.assertEqual(expected, ret)
def test_stack_preview(self):
result = self.client.stacks.preview(
template=self.random_template,
stack_name=self._stack_rand_name(),
disable_rollback=True).to_dict()
self._assert_preview_results(result)
def test_create_stack(self):
stack_identifier = self.stack_create(template=self.random_template)
stack = self.client.stacks.get(stack_identifier)
self._assert_create_results(stack.to_dict())
rl = self.client.resources.list(stack_identifier)
self.assertEqual(1, len(rl))
self._assert_resource_results(rl[0].to_dict())