Added policy to format text w/o exception for method traverse

The method traverse can be used for formatting task parameters,
but it raises exception in case if format fails. The most shell tasks
contains symbols '{', '}' and python string format tries to format such
string. This change allows to specify the safe_formatter and
leave argument as is if format fails.

This patch also improves error handling for traverse, the default
text formatter added information about text that cannot be formatted.

Change-Id: I2d398d7a27966d842812d20ad1f5e7c5ecbff676
Related-bug: #1614401
This commit is contained in:
Bulat Gaifullin 2016-03-18 23:27:35 +03:00 committed by Alexey Stupnikov
parent 77bd52b07a
commit 3198aa7b19
2 changed files with 59 additions and 6 deletions

View File

@ -26,6 +26,7 @@ from nailgun.utils import dict_merge
from nailgun.utils import flatten from nailgun.utils import flatten
from nailgun.utils import grouper from nailgun.utils import grouper
from nailgun.utils import http_get from nailgun.utils import http_get
from nailgun.utils import text_format_safe
from nailgun.utils import traverse from nailgun.utils import traverse
from nailgun.utils.debian import get_apt_preferences_line from nailgun.utils.debian import get_apt_preferences_line
@ -153,6 +154,35 @@ class TestTraverse(base.BaseUnitTest):
} }
]}) ]})
def test_formatter_returns_informative_error(self):
with self.assertRaisesRegexp(ValueError, '{a}'):
traverse(self.data, self.TestGenerator, {'b': 13})
def test_w_safe_formatting_context(self):
data = self.data.copy()
data['bar'] = 'test {b} value'
result = traverse(
data, self.TestGenerator, {'a': 13},
text_format_safe
)
self.assertEqual(result, {
'foo': 'testvalue',
'bar': 'test {b} value',
'baz': 42,
'regex': {
'source': 'test {a} string',
'error': 'an {a} error'
},
'list': [
{
'x': 'a 13 a',
},
{
'y': 'b 42 b',
}
]})
class TestGetDebianReleaseFile(base.BaseUnitTest): class TestGetDebianReleaseFile(base.BaseUnitTest):

View File

@ -91,15 +91,35 @@ def dict_merge(a, b):
return result return result
def traverse(data, generator_class, formatter_context=None): def text_format(data, context):
try:
return data.format(**context)
except Exception as e:
raise ValueError("Cannot format {0}: {1}".format(data, e))
def text_format_safe(data, context):
try:
return data.format(**context)
except Exception as e:
logger.warning("Cannot format %s: %s. it will be used as is.",
data, six.text_type(e))
return data
def traverse(data, generator_class, formatter_context=None, formatter=None):
"""Traverse data. """Traverse data.
:param data: an input data to be traversed :param data: an input data to be traversed
:param generator_class: a generator class to be used :param generator_class: a generator class to be used
:param formatter_context: a dict to be passed into .format() for strings :param formatter_context: a dict to be passed into .format() for strings
:param formatter: the text formatter, by default text_format will be used
:returns: a dict with traversed data :returns: a dict with traversed data
""" """
if formatter is None:
formatter = text_format
# generate value if generator is specified # generate value if generator is specified
if isinstance(data, collections.Mapping) and 'generator' in data: if isinstance(data, collections.Mapping) and 'generator' in data:
try: try:
@ -117,19 +137,22 @@ def traverse(data, generator_class, formatter_context=None):
# so it fails if we try to format them. as a workaround, we # so it fails if we try to format them. as a workaround, we
# can skip them and do copy as is. # can skip them and do copy as is.
if key != 'regex': if key != 'regex':
rv[key] = traverse(value, generator_class, formatter_context) rv[key] = traverse(
value, generator_class, formatter_context, formatter
)
else: else:
rv[key] = value rv[key] = value
return rv return rv
# format all strings with "formatter_context" # format all strings with "formatter_context"
elif isinstance(data, six.string_types) and formatter_context: elif isinstance(data, six.string_types) and formatter_context:
return data.format(**formatter_context) return formatter(data, formatter_context)
# we want to traverse all sequences also (lists, tuples, etc) # we want to traverse all sequences also (lists, tuples, etc)
elif isinstance(data, (list, tuple)): elif isinstance(data, (list, tuple, set)):
return type(data)( return type(data)(
(traverse(i, generator_class, formatter_context) for i in data)) traverse(i, generator_class, formatter_context, formatter)
for i in data
)
# just return value as is for all other cases # just return value as is for all other cases
return data return data