diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index 3630f83e..468fd5bf 100644
--- a/.ci/azure-pipelines.yml
+++ b/.ci/azure-pipelines.yml
@@ -27,15 +27,15 @@ jobs:
Loc_27_210:
tox.env: py27-mode_localhost-ansible2.10
- Loc_312_6:
+ Loc_312_7:
python.version: '3.12'
- tox.env: py312-mode_localhost-ansible6
+ tox.env: py312-mode_localhost-ansible7
Van_27_210:
tox.env: py27-mode_localhost-ansible2.10-strategy_linear
- Van_312_6:
+ Van_312_7:
python.version: '3.12'
- tox.env: py312-mode_localhost-ansible6-strategy_linear
+ tox.env: py312-mode_localhost-ansible7-strategy_linear
- job: Linux
pool:
@@ -147,3 +147,6 @@ jobs:
Ans_312_6:
python.version: '3.12'
tox.env: py312-mode_ansible-ansible6
+ Ans_312_7:
+ python.version: '3.12'
+ tox.env: py312-mode_ansible-ansible7
diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py
index 44caf9ac..dfc3aec4 100644
--- a/ansible_mitogen/connection.py
+++ b/ansible_mitogen/connection.py
@@ -43,7 +43,6 @@ import ansible.errors
import ansible.plugins.connection
import mitogen.core
-import mitogen.utils
import ansible_mitogen.mixins
import ansible_mitogen.parsing
@@ -51,6 +50,7 @@ import ansible_mitogen.process
import ansible_mitogen.services
import ansible_mitogen.target
import ansible_mitogen.transport_config
+import ansible_mitogen.utils.unsafe
LOG = logging.getLogger(__name__)
@@ -797,7 +797,7 @@ class Connection(ansible.plugins.connection.ConnectionBase):
call_context=self.binding.get_service_context(),
service_name='ansible_mitogen.services.ContextService',
method_name='get',
- stack=mitogen.utils.cast(list(stack)),
+ stack=ansible_mitogen.utils.unsafe.cast(list(stack)),
)
except mitogen.core.CallError:
LOG.warning('Connection failed; stack configuration was:\n%s',
@@ -848,7 +848,7 @@ class Connection(ansible.plugins.connection.ConnectionBase):
inventory_name, stack = self._build_stack()
worker_model = ansible_mitogen.process.get_worker_model()
self.binding = worker_model.get_binding(
- mitogen.utils.cast(inventory_name)
+ ansible_mitogen.utils.unsafe.cast(inventory_name)
)
self._connect_stack(stack)
@@ -933,7 +933,7 @@ class Connection(ansible.plugins.connection.ConnectionBase):
call_context=binding.get_service_context(),
service_name='ansible_mitogen.services.ContextService',
method_name='reset',
- stack=mitogen.utils.cast(list(stack)),
+ stack=ansible_mitogen.utils.unsafe.cast(list(stack)),
)
finally:
binding.close()
@@ -1011,8 +1011,8 @@ class Connection(ansible.plugins.connection.ConnectionBase):
emulate_tty = (not in_data and sudoable)
rc, stdout, stderr = self.get_chain().call(
ansible_mitogen.target.exec_command,
- cmd=mitogen.utils.cast(cmd),
- in_data=mitogen.utils.cast(in_data),
+ cmd=ansible_mitogen.utils.unsafe.cast(cmd),
+ in_data=ansible_mitogen.utils.unsafe.cast(in_data),
chdir=mitogen_chdir or self.get_default_cwd(),
emulate_tty=emulate_tty,
)
@@ -1039,7 +1039,7 @@ class Connection(ansible.plugins.connection.ConnectionBase):
ansible_mitogen.target.transfer_file(
context=self.context,
# in_path may be AnsibleUnicode
- in_path=mitogen.utils.cast(in_path),
+ in_path=ansible_mitogen.utils.unsafe.cast(in_path),
out_path=out_path
)
@@ -1057,7 +1057,7 @@ class Connection(ansible.plugins.connection.ConnectionBase):
"""
self.get_chain().call_no_reply(
ansible_mitogen.target.write_path,
- mitogen.utils.cast(out_path),
+ ansible_mitogen.utils.unsafe.cast(out_path),
mitogen.core.Blob(data),
mode=mode,
utimes=utimes,
@@ -1119,7 +1119,7 @@ class Connection(ansible.plugins.connection.ConnectionBase):
call_context=self.binding.get_service_context(),
service_name='mitogen.service.FileService',
method_name='register',
- path=mitogen.utils.cast(in_path)
+ path=ansible_mitogen.utils.unsafe.cast(in_path)
)
# For now this must remain synchronous, as the action plug-in may have
diff --git a/ansible_mitogen/loaders.py b/ansible_mitogen/loaders.py
index 1f4d8fc6..754ec6cb 100644
--- a/ansible_mitogen/loaders.py
+++ b/ansible_mitogen/loaders.py
@@ -49,7 +49,7 @@ __all__ = [
ANSIBLE_VERSION_MIN = (2, 10)
-ANSIBLE_VERSION_MAX = (2, 13)
+ANSIBLE_VERSION_MAX = (2, 14)
NEW_VERSION_MSG = (
"Your Ansible version (%s) is too recent. The most recent version\n"
diff --git a/ansible_mitogen/mixins.py b/ansible_mitogen/mixins.py
index 690998f1..9cc97a48 100644
--- a/ansible_mitogen/mixins.py
+++ b/ansible_mitogen/mixins.py
@@ -50,12 +50,12 @@ import ansible.plugins.action
import mitogen.core
import mitogen.select
-import mitogen.utils
import ansible_mitogen.connection
import ansible_mitogen.planner
import ansible_mitogen.target
import ansible_mitogen.utils
+import ansible_mitogen.utils.unsafe
from ansible.module_utils._text import to_text
@@ -187,7 +187,7 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
LOG.debug('_remote_file_exists(%r)', path)
return self._connection.get_chain().call(
ansible_mitogen.target.file_exists,
- mitogen.utils.cast(path)
+ ansible_mitogen.utils.unsafe.cast(path)
)
def _configure_module(self, module_name, module_args, task_vars=None):
@@ -324,7 +324,7 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
# ~root/.ansible -> /root/.ansible
return self._connection.get_chain(use_login=(not sudoable)).call(
os.path.expanduser,
- mitogen.utils.cast(path),
+ ansible_mitogen.utils.unsafe.cast(path),
)
def get_task_timeout_secs(self):
@@ -387,11 +387,11 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase):
ansible_mitogen.planner.Invocation(
action=self,
connection=self._connection,
- module_name=mitogen.core.to_text(module_name),
- module_args=mitogen.utils.cast(module_args),
+ module_name=ansible_mitogen.utils.unsafe.cast(mitogen.core.to_text(module_name)),
+ module_args=ansible_mitogen.utils.unsafe.cast(module_args),
task_vars=task_vars,
templar=self._templar,
- env=mitogen.utils.cast(env),
+ env=ansible_mitogen.utils.unsafe.cast(env),
wrap_async=wrap_async,
timeout_secs=self.get_task_timeout_secs(),
)
diff --git a/ansible_mitogen/services.py b/ansible_mitogen/services.py
index b0f5c70e..3e9de652 100644
--- a/ansible_mitogen/services.py
+++ b/ansible_mitogen/services.py
@@ -52,10 +52,10 @@ import ansible.constants
import mitogen.core
import mitogen.service
-import mitogen.utils
import ansible_mitogen.loaders
import ansible_mitogen.module_finder
import ansible_mitogen.target
+import ansible_mitogen.utils.unsafe
LOG = logging.getLogger(__name__)
@@ -91,7 +91,7 @@ def _get_candidate_temp_dirs():
remote_tmp = ansible.constants.DEFAULT_REMOTE_TMP
system_tmpdirs = ('/var/tmp', '/tmp')
- return mitogen.utils.cast([remote_tmp] + list(system_tmpdirs))
+ return ansible_mitogen.utils.unsafe.cast([remote_tmp] + list(system_tmpdirs))
def key_from_dict(**kwargs):
diff --git a/ansible_mitogen/utils.py b/ansible_mitogen/utils/__init__.py
similarity index 100%
rename from ansible_mitogen/utils.py
rename to ansible_mitogen/utils/__init__.py
diff --git a/ansible_mitogen/utils/unsafe.py b/ansible_mitogen/utils/unsafe.py
new file mode 100644
index 00000000..b2c3d533
--- /dev/null
+++ b/ansible_mitogen/utils/unsafe.py
@@ -0,0 +1,79 @@
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import ansible
+import ansible.utils.unsafe_proxy
+
+import ansible_mitogen.utils
+
+import mitogen
+import mitogen.core
+import mitogen.utils
+
+__all__ = [
+ 'cast',
+]
+
+def _cast_to_dict(obj): return {cast(k): cast(v) for k, v in obj.items()}
+def _cast_to_list(obj): return [cast(v) for v in obj]
+def _cast_unsafe(obj): return obj._strip_unsafe()
+def _passthrough(obj): return obj
+
+
+# A dispatch table to cast objects based on their exact type.
+# This is an optimisation, reliable fallbacks are required (e.g. isinstance())
+_CAST_DISPATCH = {
+ bytes: bytes,
+ dict: _cast_to_dict,
+ list: _cast_to_list,
+ tuple: _cast_to_list,
+ mitogen.core.UnicodeType: mitogen.core.UnicodeType,
+}
+_CAST_DISPATCH.update({t: _passthrough for t in mitogen.utils.PASSTHROUGH})
+
+if hasattr(ansible.utils.unsafe_proxy.AnsibleUnsafeText, '_strip_unsafe'):
+ _CAST_DISPATCH.update({
+ ansible.utils.unsafe_proxy.AnsibleUnsafeBytes: _cast_unsafe,
+ ansible.utils.unsafe_proxy.AnsibleUnsafeText: _cast_unsafe,
+ ansible.utils.unsafe_proxy.NativeJinjaUnsafeText: _cast_unsafe,
+ })
+elif ansible_mitogen.utils.ansible_version[:2] <= (2, 16):
+ _CAST_DISPATCH.update({
+ ansible.utils.unsafe_proxy.AnsibleUnsafeBytes: bytes,
+ ansible.utils.unsafe_proxy.AnsibleUnsafeText: mitogen.core.UnicodeType,
+ })
+else:
+ mitogen_ver = '.'.join(str(v) for v in mitogen.__version__)
+ raise ImportError("Mitogen %s can't unwrap Ansible %s AnsibleUnsafe objects"
+ % (mitogen_ver, ansible.__version__))
+
+
+def cast(obj):
+ """
+ Return obj (or a copy) with subtypes of builtins cast to their supertype.
+
+ This is an enhanced version of :func:`mitogen.utils.cast`. In addition it
+ handles ``ansible.utils.unsafe_proxy.AnsibleUnsafeText`` and variants.
+
+ There are types handled by :func:`ansible.utils.unsafe_proxy.wrap_var()`
+ that this function currently does not handle (e.g. `set()`), or preserve
+ preserve (e.g. `tuple()`). Future enhancements may change this.
+
+ :param obj:
+ Object to undecorate.
+ :returns:
+ Undecorated object.
+ """
+ # Fast path: obj is a known type, dispatch directly
+ try:
+ unwrapper = _CAST_DISPATCH[type(obj)]
+ except KeyError:
+ pass
+ else:
+ return unwrapper(obj)
+
+ # Slow path: obj is some unknown subclass
+ if isinstance(obj, dict): return _cast_to_dict(obj)
+ if isinstance(obj, (list, tuple)): return _cast_to_list(obj)
+
+ return mitogen.utils.cast(obj)
diff --git a/docs/ansible_detailed.rst b/docs/ansible_detailed.rst
index cb83aa71..09db3cd8 100644
--- a/docs/ansible_detailed.rst
+++ b/docs/ansible_detailed.rst
@@ -150,7 +150,7 @@ Noteworthy Differences
- Ansible 2.10, 3, and 4; with Python 2.7, or 3.6-3.11
- Ansible 5; with Python 3.8-3.11
- - Ansible 6; with Python 3.8-3.12
+ - Ansible 6 and 7; with Python 3.8-3.12
Verify your installation is running one of these versions by checking
``ansible --version`` output.
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 600a8680..e6fca67c 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -17,10 +17,22 @@ Release Notes
To avail of fixes in an unreleased version, please download a ZIP file
`directly from GitHub `_.
+
Unreleased
----------
+v0.3.6 (2024-04-04)
+-------------------
+
+* :gh:issue:`974` Support Ansible 7
+* :gh:issue:`1046` Raise :py:exc:`TypeError` in :func:``
+ when casting a string subtype to `bytes()` or `str()` fails. This is
+ potentially an API breaking change. Failures previously passed silently.
+* :gh:issue:`1046` Add :func:``, to cast
+ :class:`ansible.utils.unsafe_proxy.AnsibleUnsafe` objects in Ansible 7+.
+
+
v0.3.5 (2024-03-17)
-------------------
diff --git a/docs/conf.py b/docs/conf.py
index fb9974cb..0e201d44 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -2,7 +2,7 @@ import sys
sys.path.append('.')
-VERSION = '0.3.5'
+VERSION = '0.3.6'
author = u'Network Genomics'
copyright = u'2021, the Mitogen authors'
diff --git a/docs/contributors.rst b/docs/contributors.rst
index 61a9eb1b..207e4d7b 100644
--- a/docs/contributors.rst
+++ b/docs/contributors.rst
@@ -130,6 +130,7 @@ sponsorship and outstanding future-thinking of its early adopters.
luto
Mayeu a.k.a Matthieu Maury
@nathanhruby
+ Orion Poplawski
Ramy
Scott Vokes
Tom Eichhorn
diff --git a/mitogen/__init__.py b/mitogen/__init__.py
index 9bd89669..d08d9883 100644
--- a/mitogen/__init__.py
+++ b/mitogen/__init__.py
@@ -35,7 +35,7 @@ be expected. On the slave, it is built dynamically during startup.
#: Library version as a tuple.
-__version__ = (0, 3, 6, 'dev0')
+__version__ = (0, 3, 7, 'dev')
#: This is :data:`False` in slave contexts. Previously it was used to prevent
diff --git a/mitogen/utils.py b/mitogen/utils.py
index 71f7c35f..1fbf71fe 100644
--- a/mitogen/utils.py
+++ b/mitogen/utils.py
@@ -190,10 +190,13 @@ PASSTHROUGH = (
def cast(obj):
"""
+ Return obj (or a copy) with subtypes of builtins cast to their supertype.
+ Subtypes of those in :data:`PASSTHROUGH` are not modified.
+
Many tools love to subclass built-in types in order to implement useful
functionality, such as annotating the safety of a Unicode string, or adding
- additional methods to a dict. However, cPickle loves to preserve those
- subtypes during serialization, resulting in CallError during :meth:`call
+ additional methods to a dict. However :py:mod:`pickle` serializes these
+ exactly, leading to :exc:`mitogen.CallError` during :meth:`Context.call
` in the target when it tries to deserialize
the data.
@@ -201,6 +204,9 @@ def cast(obj):
custom sub-types removed. The functionality is not default since the
resulting walk may be computationally expensive given a large enough graph.
+ Raises :py:exc:`TypeError` if an unknown subtype is encountered, or
+ casting does not return the desired supertype.
+
See :ref:`serialization-rules` for a list of supported types.
:param obj:
@@ -215,8 +221,16 @@ def cast(obj):
if isinstance(obj, PASSTHROUGH):
return obj
if isinstance(obj, mitogen.core.UnicodeType):
- return mitogen.core.UnicodeType(obj)
+ return _cast(obj, mitogen.core.UnicodeType)
if isinstance(obj, mitogen.core.BytesType):
- return mitogen.core.BytesType(obj)
+ return _cast(obj, mitogen.core.BytesType)
raise TypeError("Cannot serialize: %r: %r" % (type(obj), obj))
+
+
+def _cast(obj, desired_type):
+ result = desired_type(obj)
+ if type(result) is not desired_type:
+ raise TypeError("Cast of %r to %r failed, got %r"
+ % (type(obj), desired_type, type(result)))
+ return result
diff --git a/tests/ansible/integration/action/remote_expand_user.yml b/tests/ansible/integration/action/remote_expand_user.yml
index 3a675635..97dd02f9 100644
--- a/tests/ansible/integration/action/remote_expand_user.yml
+++ b/tests/ansible/integration/action/remote_expand_user.yml
@@ -25,7 +25,7 @@
sudoable: false
register: out
- assert:
- that: out.result == '{{user_facts.ansible_facts.ansible_user_dir}}/foo'
+ that: out.result == user_facts.ansible_facts.ansible_user_dir ~ '/foo'
fail_msg: out={{out}}
- name: "Expand ~/foo with become active. ~ is become_user's home."
@@ -48,7 +48,7 @@
sudoable: false
register: out
- assert:
- that: out.result == '{{user_facts.ansible_facts.ansible_user_dir}}/foo'
+ that: out.result == user_facts.ansible_facts.ansible_user_dir ~ '/foo'
fail_msg: out={{out}}
- name: "Expanding $HOME/foo has no effect."
@@ -72,7 +72,7 @@
sudoable: true
register: out
- assert:
- that: out.result == '{{user_facts.ansible_facts.ansible_user_dir}}/foo'
+ that: out.result == user_facts.ansible_facts.ansible_user_dir ~ '/foo'
fail_msg: out={{out}}
- name: "sudoable; Expand ~/foo with become active. ~ is become_user's home."
@@ -96,7 +96,7 @@
sudoable: true
register: out
- assert:
- that: out.result == '{{user_facts.ansible_facts.ansible_user_dir}}/foo'
+ that: out.result == user_facts.ansible_facts.ansible_user_dir ~ '/foo'
fail_msg: out={{out}}
- name: "sudoable; Expanding $HOME/foo has no effect."
diff --git a/tests/ansible/integration/async/result_shell_echo_hi.yml b/tests/ansible/integration/async/result_shell_echo_hi.yml
index f327a965..8cac07a8 100644
--- a/tests/ansible/integration/async/result_shell_echo_hi.yml
+++ b/tests/ansible/integration/async/result_shell_echo_hi.yml
@@ -32,10 +32,12 @@
- async_out.invocation.module_args.creates == None
- async_out.invocation.module_args.executable == None
- async_out.invocation.module_args.removes == None
- # In Ansible 4 (ansible-core 2.11) the warn parameter is deprecated and defaults to false.
- # It's scheduled for removal in ansible-core 2.13.
- - (ansible_version.full is version("2.11", "<", strict=True) and async_out.invocation.module_args.warn == True)
+ # | Ansible <= 3 | ansible-core <= 2.10 | present | True |
+ # | Ansible 4 - 6 | ansible-core 2.11 - 2.13 | deprecated | False |
+ # | Ansible >= 7 | ansible-core >= 2.14 | absent | n/a |
+ - (ansible_version.full is version("2.14", ">=", strict=True) and async_out.invocation.module_args.warn is not defined)
or (ansible_version.full is version("2.11", ">=", strict=True) and async_out.invocation.module_args.warn == False)
+ or (async_out.invocation.module_args.warn == True)
- async_out.rc == 0
- async_out.start.startswith("20")
- async_out.stderr == "there"
diff --git a/tests/ansible/integration/stub_connections/_end_play_if_not_sudo_linux.yml b/tests/ansible/integration/stub_connections/_end_play_if_not_sudo_linux.yml
index 55997a72..a53f75ed 100644
--- a/tests/ansible/integration/stub_connections/_end_play_if_not_sudo_linux.yml
+++ b/tests/ansible/integration/stub_connections/_end_play_if_not_sudo_linux.yml
@@ -9,7 +9,7 @@
- command: sudo -n whoami
args:
- warn: false
+ warn: "{{ False if ansible_version.full is version('2.10', '<=', strict=True) else omit }}"
ignore_errors: true
register: sudo_available
diff --git a/tests/ansible/integration/stub_connections/setns_lxc.yml b/tests/ansible/integration/stub_connections/setns_lxc.yml
index 75d4207b..489f9883 100644
--- a/tests/ansible/integration/stub_connections/setns_lxc.yml
+++ b/tests/ansible/integration/stub_connections/setns_lxc.yml
@@ -27,7 +27,7 @@
localhost
args:
chdir: ../..
- warn: false
+ warn: "{{ False if ansible_version.full is version('2.10', '<=', strict=True) else omit }}"
register: result
- assert:
diff --git a/tests/ansible/integration/stub_connections/setns_lxd.yml b/tests/ansible/integration/stub_connections/setns_lxd.yml
index 3f3bc291..d654b7bc 100644
--- a/tests/ansible/integration/stub_connections/setns_lxd.yml
+++ b/tests/ansible/integration/stub_connections/setns_lxd.yml
@@ -27,7 +27,7 @@
localhost
args:
chdir: ../..
- warn: false
+ warn: "{{ False if ansible_version.full is version('2.10', '<=', strict=True) else omit }}"
register: result
- assert:
diff --git a/tests/ansible/tests/utils_test.py b/tests/ansible/tests/utils_test.py
new file mode 100644
index 00000000..05ee8bf0
--- /dev/null
+++ b/tests/ansible/tests/utils_test.py
@@ -0,0 +1,11 @@
+import unittest
+
+import ansible_mitogen.utils
+
+
+class AnsibleVersionTest(unittest.TestCase):
+ def test_ansible_version(self):
+ self.assertIsInstance(ansible_mitogen.utils.ansible_version, tuple)
+ self.assertIsInstance(ansible_mitogen.utils.ansible_version[0], int)
+ self.assertIsInstance(ansible_mitogen.utils.ansible_version[1], int)
+ self.assertEqual(2, ansible_mitogen.utils.ansible_version[0])
diff --git a/tests/ansible/tests/utils_unsafe_test.py b/tests/ansible/tests/utils_unsafe_test.py
new file mode 100644
index 00000000..a020f55b
--- /dev/null
+++ b/tests/ansible/tests/utils_unsafe_test.py
@@ -0,0 +1,92 @@
+import unittest
+
+from ansible.utils.unsafe_proxy import AnsibleUnsafeBytes
+from ansible.utils.unsafe_proxy import AnsibleUnsafeText
+from ansible.utils.unsafe_proxy import wrap_var
+
+import ansible_mitogen.utils.unsafe
+
+import mitogen.core
+
+
+class Bytes(bytes): pass
+class Dict(dict): pass
+class List(list): pass
+class Set(set): pass
+class Text(mitogen.core.UnicodeType): pass
+class Tuple(tuple): pass
+
+
+class CastTest(unittest.TestCase):
+ def assertIsType(self, obj, cls, msg=None):
+ self.assertIs(type(obj), cls, msg)
+
+ def assertUnchanged(self, obj):
+ self.assertIs(ansible_mitogen.utils.unsafe.cast(obj), obj)
+
+ def assertCasts(self, obj, expected):
+ cast = ansible_mitogen.utils.unsafe.cast
+ self.assertEqual(cast(obj), expected)
+ self.assertIsType(cast(obj), type(expected))
+
+ def test_ansible_unsafe(self):
+ self.assertCasts(AnsibleUnsafeBytes(b'abc'), b'abc')
+ self.assertCasts(AnsibleUnsafeText(u'abc'), u'abc')
+
+ def test_passthrough(self):
+ self.assertUnchanged(0)
+ self.assertUnchanged(0.0)
+ self.assertUnchanged(False)
+ self.assertUnchanged(True)
+ self.assertUnchanged(None)
+ self.assertUnchanged(b'')
+ self.assertUnchanged(u'')
+
+ def test_builtins_roundtrip(self):
+ self.assertCasts(wrap_var(b''), b'')
+ self.assertCasts(wrap_var({}), {})
+ self.assertCasts(wrap_var([]), [])
+ self.assertCasts(wrap_var(u''), u'')
+ self.assertCasts(wrap_var(()), [])
+
+ def test_subtypes_roundtrip(self):
+ self.assertCasts(wrap_var(Bytes()), b'')
+ self.assertCasts(wrap_var(Dict()), {})
+ self.assertCasts(wrap_var(List()), [])
+ self.assertCasts(wrap_var(Text()), u'')
+ self.assertCasts(wrap_var(Tuple()), [])
+
+ def test_subtype_nested_dict(self):
+ obj = Dict(foo=Dict(bar=u'abc'))
+ wrapped = wrap_var(obj)
+ unwrapped = ansible_mitogen.utils.unsafe.cast(wrapped)
+ self.assertEqual(unwrapped, {'foo': {'bar': u'abc'}})
+ self.assertIsType(unwrapped, dict)
+ self.assertIsType(unwrapped['foo'], dict)
+ self.assertIsType(unwrapped['foo']['bar'], mitogen.core.UnicodeType)
+
+ def test_subtype_roundtrip_list(self):
+ # wrap_var() preserves sequence types, cast() does not (for now)
+ obj = List([List([u'abc'])])
+ wrapped = wrap_var(obj)
+ unwrapped = ansible_mitogen.utils.unsafe.cast(wrapped)
+ self.assertEqual(unwrapped, [[u'abc']])
+ self.assertIsType(unwrapped, list)
+ self.assertIsType(unwrapped[0], list)
+ self.assertIsType(unwrapped[0][0], mitogen.core.UnicodeType)
+
+ def test_subtype_roundtrip_tuple(self):
+ # wrap_var() preserves sequence types, cast() does not (for now)
+ obj = Tuple([Tuple([u'abc'])])
+ wrapped = wrap_var(obj)
+ unwrapped = ansible_mitogen.utils.unsafe.cast(wrapped)
+ self.assertEqual(unwrapped, [[u'abc']])
+ self.assertIsType(unwrapped, list)
+ self.assertIsType(unwrapped[0], list)
+ self.assertIsType(unwrapped[0][0], mitogen.core.UnicodeType)
+
+ def test_unknown_types_raise(self):
+ cast = ansible_mitogen.utils.unsafe.cast
+ self.assertRaises(TypeError, cast, set())
+ self.assertRaises(TypeError, cast, Set())
+ self.assertRaises(TypeError, cast, 4j)
diff --git a/tests/image_prep/_container_setup.yml b/tests/image_prep/_container_setup.yml
index 353a7d5b..d41d1326 100644
--- a/tests/image_prep/_container_setup.yml
+++ b/tests/image_prep/_container_setup.yml
@@ -57,7 +57,7 @@
dnf: dnf clean all
command: "{{ clean_command[ansible_pkg_mgr] }}"
args:
- warn: false
+ warn: "{{ False if ansible_version.full is version('2.10', '<=', strict=True) else omit }}"
- name: Clean up apt package lists
shell: rm -rf {{item}}/*
diff --git a/tests/utils_test.py b/tests/utils_test.py
index 6c71d33c..37246961 100644
--- a/tests/utils_test.py
+++ b/tests/utils_test.py
@@ -44,6 +44,44 @@ class Unicode(mitogen.core.UnicodeType): pass
class Bytes(mitogen.core.BytesType): pass
+class StubbornBytes(mitogen.core.BytesType):
+ """
+ A binary string type that persists through `bytes(...)`.
+
+ Stand-in for `AnsibleUnsafeBytes()` in Ansible 7-9 (core 2.14-2.16), after
+ fixes/mitigations for CVE-2023-5764.
+ """
+ if mitogen.core.PY3:
+ def __bytes__(self): return self
+ def __str__(self): return self.decode()
+ else:
+ def __str__(self): return self
+ def __unicode__(self): return self.decode()
+
+ def decode(self, encoding='utf-8', errors='strict'):
+ s = super(StubbornBytes).encode(encoding=encoding, errors=errors)
+ return StubbornText(s)
+
+
+class StubbornText(mitogen.core.UnicodeType):
+ """
+ A text string type that persists through `unicode(...)` or `str(...)`.
+
+ Stand-in for `AnsibleUnsafeText()` in Ansible 7-9 (core 2.14-2.16), after
+ following fixes/mitigations for CVE-2023-5764.
+ """
+ if mitogen.core.PY3:
+ def __bytes__(self): return self.encode()
+ def __str__(self): return self
+ else:
+ def __str__(self): return self.encode()
+ def __unicode__(self): return self
+
+ def encode(self, encoding='utf-8', errors='strict'):
+ s = super(StubbornText).encode(encoding=encoding, errors=errors)
+ return StubbornBytes(s)
+
+
class CastTest(testlib.TestCase):
def test_dict(self):
self.assertEqual(type(mitogen.utils.cast({})), dict)
@@ -91,6 +129,15 @@ class CastTest(testlib.TestCase):
self.assertEqual(type(mitogen.utils.cast(b(''))), mitogen.core.BytesType)
self.assertEqual(type(mitogen.utils.cast(Bytes())), mitogen.core.BytesType)
+ def test_stubborn_types_raise(self):
+ stubborn_bytes = StubbornBytes(b('abc'))
+ self.assertIs(stubborn_bytes, mitogen.core.BytesType(stubborn_bytes))
+ self.assertRaises(TypeError, mitogen.utils.cast, stubborn_bytes)
+
+ stubborn_text = StubbornText(u'abc')
+ self.assertIs(stubborn_text, mitogen.core.UnicodeType(stubborn_text))
+ self.assertRaises(TypeError, mitogen.utils.cast, stubborn_text)
+
def test_unknown(self):
self.assertRaises(TypeError, mitogen.utils.cast, set())
self.assertRaises(TypeError, mitogen.utils.cast, 4j)
diff --git a/tox.ini b/tox.ini
index 4a1772a0..f2b46413 100644
--- a/tox.ini
+++ b/tox.ini
@@ -57,7 +57,7 @@ envlist =
init,
py{27,36}-mode_ansible-ansible{2.10,3,4},
py{311}-mode_ansible-ansible{2.10,3,4,5},
- py{312}-mode_ansible-ansible{6},
+ py{312}-mode_ansible-ansible{6,7},
py{27,36,312}-mode_mitogen-distro_centos{6,7,8},
py{27,36,312}-mode_mitogen-distro_debian{9,10,11},
py{27,36,312}-mode_mitogen-distro_ubuntu{1604,1804,2004},
@@ -83,6 +83,7 @@ deps =
ansible4: ansible==4.10.0
ansible5: ansible~=5.0
ansible6: ansible~=6.0
+ ansible7: ansible~=7.0
install_command =
python -m pip --no-python-version-warning --disable-pip-version-check install {opts} {packages}
commands_pre =
@@ -118,8 +119,9 @@ setenv =
distro_ubuntu1804: DISTRO=ubuntu1804
distro_ubuntu2004: DISTRO=ubuntu2004
# Note the plural, only applicable to MODE=ansible
- # Ansible 6 (ansible-core 2.13) requires Python >= 2.7 or >= 3.5 on targets
+ # Ansible >= 6 (ansible-core >= 2.13) require Python 2.7 or >= 3.5 on targets
ansible6: DISTROS=centos7 centos8 debian9 debian10 debian11 ubuntu1604 ubuntu1804 ubuntu2004
+ ansible7: DISTROS=centos7 centos8 debian9 debian10 debian11 ubuntu1604 ubuntu1804 ubuntu2004
distros_centos: DISTROS=centos6 centos7 centos8
distros_centos5: DISTROS=centos5
distros_centos6: DISTROS=centos6