Skip to content

Commit 5b2ad81

Browse files
committed
[stable-2.7] Fix for callback plugins on Python3 when a module returns dictionary keys that aren't strings
This fixes one of the problems reported in ansible#49343 Upstream Python3 bug for the json traceback: https://bugs.python.org/issue25457 and PR that may fix it: python/cpython#8011. (cherry picked from commit c817bef) Co-authored-by: Toshio Kuratomi <a.badger@gmail.com>
1 parent 85764a0 commit 5b2ad81

3 files changed

Lines changed: 49 additions & 17 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
bugfixes:
3+
- Fix for callback plugins on Python3 when a module returns non-string field
4+
names in its results. (https://github.com/ansible/ansible/issues/49343)

‎lib/ansible/plugins/callback/__init__.py‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,14 @@
3535
from ansible.utils.color import stringc
3636
from ansible.vars.clean import strip_internal_keys
3737

38-
try:
39-
from __main__ import display as global_display
40-
except ImportError:
41-
from ansible.utils.display import Display
42-
global_display = Display()
38+
if PY3:
39+
# OrderedDict is needed for a backwards compat shim on Python3.x only
40+
# https://github.com/ansible/ansible/pull/49512
41+
from collections import OrderedDict
42+
else:
43+
OrderedDict = None
44+
45+
global_display = Display()
4346

4447
try:
4548
from __main__ import cli
@@ -120,7 +123,18 @@ def _dump_results(self, result, indent=None, sort_keys=True, keep_invocation=Fal
120123
if 'exception' in abridged_result:
121124
del abridged_result['exception']
122125

123-
return json.dumps(abridged_result, cls=AnsibleJSONEncoder, indent=indent, ensure_ascii=False, sort_keys=sort_keys)
126+
try:
127+
jsonified_results = json.dumps(abridged_result, cls=AnsibleJSONEncoder, indent=indent, ensure_ascii=False, sort_keys=sort_keys)
128+
except TypeError:
129+
# Python3 bug: throws an exception when keys are non-homogenous types:
130+
# https://bugs.python.org/issue25457
131+
# sort into an OrderedDict and then json.dumps() that instead
132+
if not OrderedDict:
133+
raise
134+
jsonified_results = json.dumps(OrderedDict(sorted(abridged_result.items(), key=to_text)),
135+
cls=AnsibleJSONEncoder, indent=indent,
136+
ensure_ascii=False, sort_keys=False)
137+
return jsonified_results
124138

125139
def _handle_warnings(self, res):
126140
''' display warnings, if enabled and any exist in the result '''

‎test/units/plugins/callback/test_callback.py‎

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919
from __future__ import (absolute_import, division, print_function)
2020
__metaclass__ = type
2121

22+
import json
2223
import re
2324
import textwrap
2425
import types
2526

2627
from ansible.compat.tests import unittest
2728
from ansible.compat.tests.mock import patch, mock_open, MagicMock
2829

30+
import pytest
2931

3032
from ansible.plugins.callback import CallbackBase
3133

@@ -133,7 +135,7 @@ def test_clean_results(self):
133135
self.assertEqual(result, expected_result)
134136

135137

136-
class TestCallbackDumpResults(unittest.TestCase):
138+
class TestCallbackDumpResults(object):
137139
def test_internal_keys(self):
138140
cb = CallbackBase()
139141
result = {'item': 'some_item',
@@ -144,35 +146,47 @@ def test_internal_keys(self):
144146
'bad_dict_key': {'_ansible_internal_blah': 'SENTINEL'},
145147
'changed': True}
146148
json_out = cb._dump_results(result)
147-
self.assertFalse('"_ansible_' in json_out)
148-
self.assertFalse('SENTINEL' in json_out)
149-
self.assertTrue('LEFTIN' in json_out)
149+
assert '"_ansible_' not in json_out
150+
assert 'SENTINEL' not in json_out
151+
assert 'LEFTIN' in json_out
150152

151153
def test_exception(self):
152154
cb = CallbackBase()
153155
result = {'item': 'some_item LEFTIN',
154156
'exception': ['frame1', 'SENTINEL']}
155157
json_out = cb._dump_results(result)
156-
self.assertFalse('SENTINEL' in json_out)
157-
self.assertFalse('exception' in json_out)
158-
self.assertTrue('LEFTIN' in json_out)
158+
assert 'SENTINEL' not in json_out
159+
assert 'exception' not in json_out
160+
assert 'LEFTIN' in json_out
159161

160162
def test_verbose(self):
161163
cb = CallbackBase()
162164
result = {'item': 'some_item LEFTIN',
163165
'_ansible_verbose_always': 'chicane'}
164166
json_out = cb._dump_results(result)
165-
self.assertFalse('SENTINEL' in json_out)
166-
self.assertTrue('LEFTIN' in json_out)
167+
assert 'SENTINEL' not in json_out
168+
assert 'LEFTIN' in json_out
167169

168170
def test_diff(self):
169171
cb = CallbackBase()
170172
result = {'item': 'some_item LEFTIN',
171173
'diff': ['remove stuff', 'added LEFTIN'],
172174
'_ansible_verbose_always': 'chicane'}
173175
json_out = cb._dump_results(result)
174-
self.assertFalse('SENTINEL' in json_out)
175-
self.assertTrue('LEFTIN' in json_out)
176+
assert 'SENTINEL' not in json_out
177+
assert 'LEFTIN' in json_out
178+
179+
def test_mixed_keys(self):
180+
cb = CallbackBase()
181+
result = {3: 'pi',
182+
'tau': 6}
183+
json_out = cb._dump_results(result)
184+
round_trip_result = json.loads(json_out)
185+
assert len(round_trip_result) == 2
186+
assert '3' in round_trip_result
187+
assert 'tau' in round_trip_result
188+
assert round_trip_result['3'] == 'pi'
189+
assert round_trip_result['tau'] == 6
176190

177191

178192
# TODO: triggr the 'except UnicodeError' around _get_diff

0 commit comments

Comments
 (0)