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:
parent
77bd52b07a
commit
3198aa7b19
@ -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):
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user