- ensure we preserve the typeerror part of the exception so loop defereed error handling
can postpone those caused by undefined variables until the when check is done.
- fix tests to comply with the 'new normal'
- human_to_bytes and others can issue TypeError not only on 'non string'
but also bad string that is not convertable.
Co-authored-by: Sloane Hertel <shertel@redhat.com>
Co-authored-by: Sloane Hertel <shertel@redhat.com>
(cherry picked from commit cf89ca8a03a8a84302ad27cb1fc7aa9120b743ca)
... | ... |
@@ -312,3 +312,8 @@ class AnsibleActionFail(AnsibleAction): |
312 | 312 |
class _AnsibleActionDone(AnsibleAction): |
313 | 313 |
''' an action runtime early exit''' |
314 | 314 |
pass |
315 |
+ |
|
316 |
+ |
|
317 |
+class AnsibleFilterTypeError(AnsibleTemplateError, TypeError): |
|
318 |
+ ''' a Jinja filter templating failure due to bad type''' |
|
319 |
+ pass |
... | ... |
@@ -40,7 +40,7 @@ from random import Random, SystemRandom, shuffle |
40 | 40 |
|
41 | 41 |
from jinja2.filters import environmentfilter, do_groupby as _do_groupby |
42 | 42 |
|
43 |
-from ansible.errors import AnsibleError, AnsibleFilterError |
|
43 |
+from ansible.errors import AnsibleError, AnsibleFilterError, AnsibleFilterTypeError |
|
44 | 44 |
from ansible.module_utils.six import iteritems, string_types, integer_types, reraise |
45 | 45 |
from ansible.module_utils.six.moves import reduce, shlex_quote |
46 | 46 |
from ansible.module_utils._text import to_bytes, to_native, to_text |
... | ... |
@@ -490,7 +490,7 @@ def subelements(obj, subelements, skip_missing=False): |
490 | 490 |
elif isinstance(subelements, string_types): |
491 | 491 |
subelement_list = subelements.split('.') |
492 | 492 |
else: |
493 |
- raise AnsibleFilterError('subelements must be a list or a string') |
|
493 |
+ raise AnsibleFilterTypeError('subelements must be a list or a string') |
|
494 | 494 |
|
495 | 495 |
results = [] |
496 | 496 |
|
... | ... |
@@ -505,9 +505,9 @@ def subelements(obj, subelements, skip_missing=False): |
505 | 505 |
break |
506 | 506 |
raise AnsibleFilterError("could not find %r key in iterated item %r" % (subelement, values)) |
507 | 507 |
except TypeError: |
508 |
- raise AnsibleFilterError("the key %s should point to a dictionary, got '%s'" % (subelement, values)) |
|
508 |
+ raise AnsibleFilterTypeError("the key %s should point to a dictionary, got '%s'" % (subelement, values)) |
|
509 | 509 |
if not isinstance(values, list): |
510 |
- raise AnsibleFilterError("the key %r should point to a list, got %r" % (subelement, values)) |
|
510 |
+ raise AnsibleFilterTypeError("the key %r should point to a list, got %r" % (subelement, values)) |
|
511 | 511 |
|
512 | 512 |
for value in values: |
513 | 513 |
results.append((element, value)) |
... | ... |
@@ -520,7 +520,7 @@ def dict_to_list_of_dict_key_value_elements(mydict, key_name='key', value_name=' |
520 | 520 |
with each having a 'key' and 'value' keys that correspond to the keys and values of the original ''' |
521 | 521 |
|
522 | 522 |
if not isinstance(mydict, Mapping): |
523 |
- raise AnsibleFilterError("dict2items requires a dictionary, got %s instead." % type(mydict)) |
|
523 |
+ raise AnsibleFilterTypeError("dict2items requires a dictionary, got %s instead." % type(mydict)) |
|
524 | 524 |
|
525 | 525 |
ret = [] |
526 | 526 |
for key in mydict: |
... | ... |
@@ -533,7 +533,7 @@ def list_of_dict_key_value_elements_to_dict(mylist, key_name='key', value_name=' |
533 | 533 |
effectively as the reverse of dict2items ''' |
534 | 534 |
|
535 | 535 |
if not is_sequence(mylist): |
536 |
- raise AnsibleFilterError("items2dict requires a list, got %s instead." % type(mylist)) |
|
536 |
+ raise AnsibleFilterTypeError("items2dict requires a list, got %s instead." % type(mylist)) |
|
537 | 537 |
|
538 | 538 |
return dict((item[key_name], item[value_name]) for item in mylist) |
539 | 539 |
|
... | ... |
@@ -28,7 +28,7 @@ import math |
28 | 28 |
|
29 | 29 |
from jinja2.filters import environmentfilter |
30 | 30 |
|
31 |
-from ansible.errors import AnsibleFilterError |
|
31 |
+from ansible.errors import AnsibleFilterError, AnsibleFilterTypeError |
|
32 | 32 |
from ansible.module_utils.common.text import formatters |
33 | 33 |
from ansible.module_utils.six import binary_type, text_type |
34 | 34 |
from ansible.module_utils.six.moves import zip, zip_longest |
... | ... |
@@ -140,14 +140,14 @@ def logarithm(x, base=math.e): |
140 | 140 |
else: |
141 | 141 |
return math.log(x, base) |
142 | 142 |
except TypeError as e: |
143 |
- raise AnsibleFilterError('log() can only be used on numbers: %s' % to_native(e)) |
|
143 |
+ raise AnsibleFilterTypeError('log() can only be used on numbers: %s' % to_native(e)) |
|
144 | 144 |
|
145 | 145 |
|
146 | 146 |
def power(x, y): |
147 | 147 |
try: |
148 | 148 |
return math.pow(x, y) |
149 | 149 |
except TypeError as e: |
150 |
- raise AnsibleFilterError('pow() can only be used on numbers: %s' % to_native(e)) |
|
150 |
+ raise AnsibleFilterTypeError('pow() can only be used on numbers: %s' % to_native(e)) |
|
151 | 151 |
|
152 | 152 |
|
153 | 153 |
def inversepower(x, base=2): |
... | ... |
@@ -157,13 +157,15 @@ def inversepower(x, base=2): |
157 | 157 |
else: |
158 | 158 |
return math.pow(x, 1.0 / float(base)) |
159 | 159 |
except (ValueError, TypeError) as e: |
160 |
- raise AnsibleFilterError('root() can only be used on numbers: %s' % to_native(e)) |
|
160 |
+ raise AnsibleFilterTypeError('root() can only be used on numbers: %s' % to_native(e)) |
|
161 | 161 |
|
162 | 162 |
|
163 | 163 |
def human_readable(size, isbits=False, unit=None): |
164 | 164 |
''' Return a human readable string ''' |
165 | 165 |
try: |
166 | 166 |
return formatters.bytes_to_human(size, isbits, unit) |
167 |
+ except TypeError as e: |
|
168 |
+ raise AnsibleFilterTypeError("human_readable() failed on bad input: %s" % to_native(e)) |
|
167 | 169 |
except Exception: |
168 | 170 |
raise AnsibleFilterError("human_readable() can't interpret following string: %s" % size) |
169 | 171 |
|
... | ... |
@@ -172,6 +174,8 @@ def human_to_bytes(size, default_unit=None, isbits=False): |
172 | 172 |
''' Return bytes count from a human readable string ''' |
173 | 173 |
try: |
174 | 174 |
return formatters.human_to_bytes(size, default_unit, isbits) |
175 |
+ except TypeError as e: |
|
176 |
+ raise AnsibleFilterTypeError("human_to_bytes() failed on bad input: %s" % to_native(e)) |
|
175 | 177 |
except Exception: |
176 | 178 |
raise AnsibleFilterError("human_to_bytes() can't interpret following string: %s" % size) |
177 | 179 |
|
... | ... |
@@ -195,16 +199,18 @@ def rekey_on_member(data, key, duplicates='error'): |
195 | 195 |
elif isinstance(data, Iterable) and not isinstance(data, (text_type, binary_type)): |
196 | 196 |
iterate_over = data |
197 | 197 |
else: |
198 |
- raise AnsibleFilterError("Type is not a valid list, set, or dict") |
|
198 |
+ raise AnsibleFilterTypeError("Type is not a valid list, set, or dict") |
|
199 | 199 |
|
200 | 200 |
for item in iterate_over: |
201 | 201 |
if not isinstance(item, Mapping): |
202 |
- raise AnsibleFilterError("List item is not a valid dict") |
|
202 |
+ raise AnsibleFilterTypeError("List item is not a valid dict") |
|
203 | 203 |
|
204 | 204 |
try: |
205 | 205 |
key_elem = item[key] |
206 | 206 |
except KeyError: |
207 | 207 |
raise AnsibleFilterError("Key {0} was not found".format(key)) |
208 |
+ except TypeError as e: |
|
209 |
+ raise AnsibleFilterTypeError(to_native(e)) |
|
208 | 210 |
except Exception as e: |
209 | 211 |
raise AnsibleFilterError(to_native(e)) |
210 | 212 |
|
211 | 213 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,29 @@ |
0 |
+- hosts: localhost |
|
1 |
+ gather_facts: false |
|
2 |
+ tasks: |
|
3 |
+ - debug: msg={{item}} |
|
4 |
+ with_dict: '{{myundef}}' |
|
5 |
+ when: |
|
6 |
+ - myundef is defined |
|
7 |
+ register: shouldskip |
|
8 |
+ |
|
9 |
+ - name: check if skipped |
|
10 |
+ assert: |
|
11 |
+ that: |
|
12 |
+ - shouldskip is skipped |
|
13 |
+ |
|
14 |
+ - debug: msg={{item}} |
|
15 |
+ loop: '{{myundef|dict2items}}' |
|
16 |
+ when: |
|
17 |
+ - myundef is defined |
|
18 |
+ |
|
19 |
+ - debug: msg={{item}} |
|
20 |
+ with_dict: '{{myundef}}' |
|
21 |
+ register: notskipped |
|
22 |
+ ignore_errors: true |
|
23 |
+ |
|
24 |
+ - name: check it failed |
|
25 |
+ assert: |
|
26 |
+ that: |
|
27 |
+ - notskipped is not skipped |
|
28 |
+ - notskipped is failed |
... | ... |
@@ -9,7 +9,7 @@ import pytest |
9 | 9 |
from jinja2 import Environment |
10 | 10 |
|
11 | 11 |
import ansible.plugins.filter.mathstuff as ms |
12 |
-from ansible.errors import AnsibleFilterError |
|
12 |
+from ansible.errors import AnsibleFilterError, AnsibleFilterTypeError |
|
13 | 13 |
|
14 | 14 |
|
15 | 15 |
UNIQUE_DATA = (([1, 3, 4, 2], sorted([1, 2, 3, 4])), |
... | ... |
@@ -79,9 +79,9 @@ class TestMax: |
79 | 79 |
class TestLogarithm: |
80 | 80 |
def test_log_non_number(self): |
81 | 81 |
# Message changed in python3.6 |
82 |
- with pytest.raises(AnsibleFilterError, match='log\\(\\) can only be used on numbers: (a float is required|must be real number, not str)'): |
|
82 |
+ with pytest.raises(AnsibleFilterTypeError, match='log\\(\\) can only be used on numbers: (a float is required|must be real number, not str)'): |
|
83 | 83 |
ms.logarithm('a') |
84 |
- with pytest.raises(AnsibleFilterError, match='log\\(\\) can only be used on numbers: (a float is required|must be real number, not str)'): |
|
84 |
+ with pytest.raises(AnsibleFilterTypeError, match='log\\(\\) can only be used on numbers: (a float is required|must be real number, not str)'): |
|
85 | 85 |
ms.logarithm(10, base='a') |
86 | 86 |
|
87 | 87 |
def test_log_ten(self): |
... | ... |
@@ -98,10 +98,10 @@ class TestLogarithm: |
98 | 98 |
class TestPower: |
99 | 99 |
def test_power_non_number(self): |
100 | 100 |
# Message changed in python3.6 |
101 |
- with pytest.raises(AnsibleFilterError, match='pow\\(\\) can only be used on numbers: (a float is required|must be real number, not str)'): |
|
101 |
+ with pytest.raises(AnsibleFilterTypeError, match='pow\\(\\) can only be used on numbers: (a float is required|must be real number, not str)'): |
|
102 | 102 |
ms.power('a', 10) |
103 | 103 |
|
104 |
- with pytest.raises(AnsibleFilterError, match='pow\\(\\) can only be used on numbers: (a float is required|must be real number, not str)'): |
|
104 |
+ with pytest.raises(AnsibleFilterTypeError, match='pow\\(\\) can only be used on numbers: (a float is required|must be real number, not str)'): |
|
105 | 105 |
ms.power(10, 'a') |
106 | 106 |
|
107 | 107 |
def test_power_squared(self): |
... | ... |
@@ -114,13 +114,13 @@ class TestPower: |
114 | 114 |
class TestInversePower: |
115 | 115 |
def test_root_non_number(self): |
116 | 116 |
# Messages differed in python-2.6, python-2.7-3.5, and python-3.6+ |
117 |
- with pytest.raises(AnsibleFilterError, match="root\\(\\) can only be used on numbers:" |
|
117 |
+ with pytest.raises(AnsibleFilterTypeError, match="root\\(\\) can only be used on numbers:" |
|
118 | 118 |
" (invalid literal for float\\(\\): a" |
119 | 119 |
"|could not convert string to float: a" |
120 | 120 |
"|could not convert string to float: 'a')"): |
121 | 121 |
ms.inversepower(10, 'a') |
122 | 122 |
|
123 |
- with pytest.raises(AnsibleFilterError, match="root\\(\\) can only be used on numbers: (a float is required|must be real number, not str)"): |
|
123 |
+ with pytest.raises(AnsibleFilterTypeError, match="root\\(\\) can only be used on numbers: (a float is required|must be real number, not str)"): |
|
124 | 124 |
ms.inversepower('a', 10) |
125 | 125 |
|
126 | 126 |
def test_square_root(self): |
... | ... |
@@ -145,27 +145,27 @@ class TestRekeyOnMember(): |
145 | 145 |
# (Input data structure, member to rekey on, expected error message) |
146 | 146 |
INVALID_ENTRIES = ( |
147 | 147 |
# Fail when key is not found |
148 |
- ([{"proto": "eigrp", "state": "enabled"}], 'invalid_key', "Key invalid_key was not found"), |
|
149 |
- ({"eigrp": {"proto": "eigrp", "state": "enabled"}}, 'invalid_key', "Key invalid_key was not found"), |
|
148 |
+ (AnsibleFilterError, [{"proto": "eigrp", "state": "enabled"}], 'invalid_key', "Key invalid_key was not found"), |
|
149 |
+ (AnsibleFilterError, {"eigrp": {"proto": "eigrp", "state": "enabled"}}, 'invalid_key', "Key invalid_key was not found"), |
|
150 | 150 |
# Fail when key is duplicated |
151 |
- ([{"proto": "eigrp"}, {"proto": "ospf"}, {"proto": "ospf"}], |
|
151 |
+ (AnsibleFilterError, [{"proto": "eigrp"}, {"proto": "ospf"}, {"proto": "ospf"}], |
|
152 | 152 |
'proto', 'Key ospf is not unique, cannot correctly turn into dict'), |
153 | 153 |
# Fail when value is not a dict |
154 |
- (["string"], 'proto', "List item is not a valid dict"), |
|
155 |
- ([123], 'proto', "List item is not a valid dict"), |
|
156 |
- ([[{'proto': 1}]], 'proto', "List item is not a valid dict"), |
|
154 |
+ (AnsibleFilterTypeError, ["string"], 'proto', "List item is not a valid dict"), |
|
155 |
+ (AnsibleFilterTypeError, [123], 'proto', "List item is not a valid dict"), |
|
156 |
+ (AnsibleFilterTypeError, [[{'proto': 1}]], 'proto', "List item is not a valid dict"), |
|
157 | 157 |
# Fail when we do not send a dict or list |
158 |
- ("string", 'proto', "Type is not a valid list, set, or dict"), |
|
159 |
- (123, 'proto', "Type is not a valid list, set, or dict"), |
|
158 |
+ (AnsibleFilterTypeError, "string", 'proto', "Type is not a valid list, set, or dict"), |
|
159 |
+ (AnsibleFilterTypeError, 123, 'proto', "Type is not a valid list, set, or dict"), |
|
160 | 160 |
) |
161 | 161 |
|
162 | 162 |
@pytest.mark.parametrize("list_original, key, expected", VALID_ENTRIES) |
163 | 163 |
def test_rekey_on_member_success(self, list_original, key, expected): |
164 | 164 |
assert ms.rekey_on_member(list_original, key) == expected |
165 | 165 |
|
166 |
- @pytest.mark.parametrize("list_original, key, expected", INVALID_ENTRIES) |
|
167 |
- def test_fail_rekey_on_member(self, list_original, key, expected): |
|
168 |
- with pytest.raises(AnsibleFilterError) as err: |
|
166 |
+ @pytest.mark.parametrize("expected_exception_type, list_original, key, expected", INVALID_ENTRIES) |
|
167 |
+ def test_fail_rekey_on_member(self, expected_exception_type, list_original, key, expected): |
|
168 |
+ with pytest.raises(expected_exception_type) as err: |
|
169 | 169 |
ms.rekey_on_member(list_original, key) |
170 | 170 |
|
171 | 171 |
assert err.value.message == expected |