From 736e8460089a622fdd8403a250f11f56400a931d Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Sun, 15 Feb 2026 21:52:07 +0900 Subject: [PATCH] Replace deprecated module_utils imports before ansible-core 2.24 removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ansible-core 2.20 deprecated `ansible.module_utils._text` and `ansible.module_utils.common._collections_compat`. Both will be removed in ansible-core 2.24. Replace all occurrences across plugins, callbacks, and tests: - `from ansible.module_utils._text import ...` → `from ansible.module_utils.common.text.converters import ...` - `from ansible.module_utils.common._collections_compat import ...` → `from collections.abc import ...` Also remove the now-unnecessary `pylint:ansible-bad-import-from` entries from the sanity ignore list for the affected files, and add a unit test that scans for these deprecated patterns to prevent regressions. Co-Authored-By: Claude Opus 4.6 --- plugins/action/patch.py | 2 +- plugins/action/synchronize.py | 4 +- plugins/callback/cgroup_perf_recap.py | 2 +- plugins/callback/json.py | 2 +- plugins/callback/jsonl.py | 2 +- plugins/modules/acl.py | 2 +- plugins/modules/authorized_key.py | 2 +- plugins/modules/firewalld_info.py | 2 +- plugins/modules/mount.py | 2 +- plugins/modules/patch.py | 2 +- plugins/modules/rhel_rpm_ostree.py | 2 +- plugins/modules/rpm_ostree_upgrade.py | 2 +- plugins/modules/seboolean.py | 2 +- plugins/modules/synchronize.py | 2 +- plugins/modules/sysctl.py | 2 +- tests/sanity/ignore-2.21.txt | 6 --- tests/unit/mock/loader.py | 2 +- tests/unit/mock/procenv.py | 2 +- tests/unit/mock/vault_helper.py | 2 +- tests/unit/modules/conftest.py | 4 +- tests/unit/modules/system/test_mount.py | 2 +- tests/unit/modules/utils.py | 2 +- tests/unit/test_deprecated_imports.py | 69 +++++++++++++++++++++++++ 23 files changed, 92 insertions(+), 29 deletions(-) create mode 100644 tests/unit/test_deprecated_imports.py diff --git a/plugins/action/patch.py b/plugins/action/patch.py index 3b477d7..ea82ed6 100644 --- a/plugins/action/patch.py +++ b/plugins/action/patch.py @@ -21,7 +21,7 @@ __metaclass__ = type import os from ansible.errors import AnsibleError, AnsibleAction, _AnsibleActionDone, AnsibleActionFail -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.parsing.convert_bool import boolean from ansible.plugins.action import ActionBase diff --git a/plugins/action/synchronize.py b/plugins/action/synchronize.py index a171a2b..6bf4ac7 100644 --- a/plugins/action/synchronize.py +++ b/plugins/action/synchronize.py @@ -22,8 +22,8 @@ import os.path from ansible import constants as C from ansible.module_utils.six import string_types from ansible.module_utils.six.moves import shlex_quote -from ansible.module_utils._text import to_text -from ansible.module_utils.common._collections_compat import MutableSequence +from ansible.module_utils.common.text.converters import to_text +from collections.abc import MutableSequence from ansible.module_utils.parsing.convert_bool import boolean from ansible.plugins.action import ActionBase from ansible.plugins.loader import connection_loader diff --git a/plugins/callback/cgroup_perf_recap.py b/plugins/callback/cgroup_perf_recap.py index 6721a03..e4136ab 100644 --- a/plugins/callback/cgroup_perf_recap.py +++ b/plugins/callback/cgroup_perf_recap.py @@ -141,7 +141,7 @@ from abc import ABCMeta, abstractmethod from functools import partial -from ansible.module_utils._text import to_bytes, to_text +from ansible.module_utils.common.text.converters import to_bytes, to_text from ansible.module_utils.six import with_metaclass from ansible.parsing.ajson import AnsibleJSONEncoder from ansible.plugins.callback import CallbackBase diff --git a/plugins/callback/json.py b/plugins/callback/json.py index 6f86bd1..1dcb6bb 100644 --- a/plugins/callback/json.py +++ b/plugins/callback/json.py @@ -48,7 +48,7 @@ import json from functools import partial from ansible.inventory.host import Host -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from ansible.parsing.ajson import AnsibleJSONEncoder from ansible.plugins.callback import CallbackBase diff --git a/plugins/callback/jsonl.py b/plugins/callback/jsonl.py index 1e03163..2d45d15 100644 --- a/plugins/callback/jsonl.py +++ b/plugins/callback/jsonl.py @@ -50,7 +50,7 @@ import copy from functools import partial from ansible.inventory.host import Host -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from ansible.parsing.ajson import AnsibleJSONEncoder from ansible.plugins.callback import CallbackBase diff --git a/plugins/modules/acl.py b/plugins/modules/acl.py index 4cc94e7..56e8de8 100644 --- a/plugins/modules/acl.py +++ b/plugins/modules/acl.py @@ -147,7 +147,7 @@ import os import platform from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native def split_entry(entry): diff --git a/plugins/modules/authorized_key.py b/plugins/modules/authorized_key.py index d712a10..51fbcc5 100644 --- a/plugins/modules/authorized_key.py +++ b/plugins/modules/authorized_key.py @@ -229,7 +229,7 @@ import errno import traceback from operator import itemgetter -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.urls import fetch_url from ansible.module_utils.six.moves.urllib.parse import urlparse diff --git a/plugins/modules/firewalld_info.py b/plugins/modules/firewalld_info.py index 0da6dd3..94654ee 100644 --- a/plugins/modules/firewalld_info.py +++ b/plugins/modules/firewalld_info.py @@ -210,7 +210,7 @@ firewalld_info: ''' from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native from ansible_collections.ansible.posix.plugins.module_utils._respawn import respawn_module, HAS_RESPAWN_UTIL from ansible_collections.ansible.posix.plugins.module_utils.version import StrictVersion diff --git a/plugins/modules/mount.py b/plugins/modules/mount.py index 6e2425b..76bbcc2 100644 --- a/plugins/modules/mount.py +++ b/plugins/modules/mount.py @@ -226,7 +226,7 @@ import platform from ansible.module_utils.basic import AnsibleModule from ansible_collections.ansible.posix.plugins.module_utils.mount import ismount from ansible.module_utils.six import iteritems -from ansible.module_utils._text import to_bytes, to_native +from ansible.module_utils.common.text.converters import to_bytes, to_native from ansible.module_utils.parsing.convert_bool import boolean diff --git a/plugins/modules/patch.py b/plugins/modules/patch.py index 39744a7..2ecd262 100644 --- a/plugins/modules/patch.py +++ b/plugins/modules/patch.py @@ -106,7 +106,7 @@ import os import platform from traceback import format_exc from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native class PatchError(Exception): diff --git a/plugins/modules/rhel_rpm_ostree.py b/plugins/modules/rhel_rpm_ostree.py index 0aafb54..dfab26a 100644 --- a/plugins/modules/rhel_rpm_ostree.py +++ b/plugins/modules/rhel_rpm_ostree.py @@ -73,7 +73,7 @@ import os import traceback from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text def locally_installed(module, pkgname): diff --git a/plugins/modules/rpm_ostree_upgrade.py b/plugins/modules/rpm_ostree_upgrade.py index 16689ca..40f9720 100644 --- a/plugins/modules/rpm_ostree_upgrade.py +++ b/plugins/modules/rpm_ostree_upgrade.py @@ -71,7 +71,7 @@ import os import traceback from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native, to_text +from ansible.module_utils.common.text.converters import to_native, to_text def rpm_ostree_transaction(module): diff --git a/plugins/modules/seboolean.py b/plugins/modules/seboolean.py index 8580c62..a23c473 100644 --- a/plugins/modules/seboolean.py +++ b/plugins/modules/seboolean.py @@ -73,7 +73,7 @@ except ImportError: HAVE_SEMANAGE = False from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils._text import to_text +from ansible.module_utils.common.text.converters import to_text from ansible_collections.ansible.posix.plugins.module_utils._respawn import respawn_module, HAS_RESPAWN_UTIL diff --git a/plugins/modules/synchronize.py b/plugins/modules/synchronize.py index d65e08f..ad32cce 100644 --- a/plugins/modules/synchronize.py +++ b/plugins/modules/synchronize.py @@ -368,7 +368,7 @@ import os import errno from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_bytes, to_native +from ansible.module_utils.common.text.converters import to_bytes, to_native from ansible.module_utils.six.moves import shlex_quote diff --git a/plugins/modules/sysctl.py b/plugins/modules/sysctl.py index 86712db..798476f 100644 --- a/plugins/modules/sysctl.py +++ b/plugins/modules/sysctl.py @@ -112,7 +112,7 @@ import tempfile from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.six import string_types from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE, BOOLEANS_TRUE -from ansible.module_utils._text import to_native +from ansible.module_utils.common.text.converters import to_native class SysctlModule(object): diff --git a/tests/sanity/ignore-2.21.txt b/tests/sanity/ignore-2.21.txt index 9b5e162..32ceadb 100644 --- a/tests/sanity/ignore-2.21.txt +++ b/tests/sanity/ignore-2.21.txt @@ -1,10 +1,4 @@ tests/utils/shippable/timing.py shebang -plugins/action/synchronize.py pylint:ansible-bad-import-from -plugins/callback/cgroup_perf_recap.py pylint:ansible-bad-import-from -plugins/modules/mount.py pylint:ansible-bad-import-from -plugins/modules/sysctl.py pylint:ansible-bad-import-from plugins/shell/csh.py pylint:ansible-bad-import-from plugins/shell/fish.py pylint:ansible-bad-import-from -tests/unit/mock/procenv.py pylint:ansible-bad-import-from tests/unit/mock/yaml_helper.py pylint:ansible-bad-import-from -tests/unit/modules/conftest.py pylint:ansible-bad-import-from diff --git a/tests/unit/mock/loader.py b/tests/unit/mock/loader.py index edeac45..ed59720 100644 --- a/tests/unit/mock/loader.py +++ b/tests/unit/mock/loader.py @@ -23,7 +23,7 @@ import os from ansible.errors import AnsibleParserError from ansible.parsing.dataloader import DataLoader -from ansible.module_utils._text import to_bytes, to_text +from ansible.module_utils.common.text.converters import to_bytes, to_text class DictDataLoader(DataLoader): diff --git a/tests/unit/mock/procenv.py b/tests/unit/mock/procenv.py index 4740452..c718c7a 100644 --- a/tests/unit/mock/procenv.py +++ b/tests/unit/mock/procenv.py @@ -27,7 +27,7 @@ from contextlib import contextmanager from io import BytesIO, StringIO from ansible_collections.ansible.posix.tests.unit.compat import unittest from ansible.module_utils.six import PY3 -from ansible.module_utils._text import to_bytes +from ansible.module_utils.common.text.converters import to_bytes @contextmanager diff --git a/tests/unit/mock/vault_helper.py b/tests/unit/mock/vault_helper.py index dcce9c7..5b2fdd2 100644 --- a/tests/unit/mock/vault_helper.py +++ b/tests/unit/mock/vault_helper.py @@ -15,7 +15,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from ansible.module_utils._text import to_bytes +from ansible.module_utils.common.text.converters import to_bytes from ansible.parsing.vault import VaultSecret diff --git a/tests/unit/modules/conftest.py b/tests/unit/modules/conftest.py index 6eb5883..a4d1ced 100644 --- a/tests/unit/modules/conftest.py +++ b/tests/unit/modules/conftest.py @@ -10,8 +10,8 @@ import json import pytest from ansible.module_utils.six import string_types -from ansible.module_utils._text import to_bytes -from ansible.module_utils.common._collections_compat import MutableMapping +from ansible.module_utils.common.text.converters import to_bytes +from collections.abc import MutableMapping @pytest.fixture diff --git a/tests/unit/modules/system/test_mount.py b/tests/unit/modules/system/test_mount.py index 73086cc..b7390a1 100644 --- a/tests/unit/modules/system/test_mount.py +++ b/tests/unit/modules/system/test_mount.py @@ -7,7 +7,7 @@ import tempfile from ansible_collections.ansible.posix.tests.unit.compat import unittest from ansible_collections.ansible.posix.tests.unit.compat.mock import MagicMock -from ansible.module_utils._text import to_bytes +from ansible.module_utils.common.text.converters import to_bytes from ansible_collections.ansible.posix.plugins.modules.mount import ( get_linux_mounts, diff --git a/tests/unit/modules/utils.py b/tests/unit/modules/utils.py index 342d528..bcfb91f 100644 --- a/tests/unit/modules/utils.py +++ b/tests/unit/modules/utils.py @@ -7,7 +7,7 @@ import json from ansible_collections.ansible.posix.tests.unit.compat import unittest from ansible_collections.ansible.posix.tests.unit.compat.mock import patch from ansible.module_utils import basic -from ansible.module_utils._text import to_bytes +from ansible.module_utils.common.text.converters import to_bytes def set_module_args(args): diff --git a/tests/unit/test_deprecated_imports.py b/tests/unit/test_deprecated_imports.py new file mode 100644 index 0000000..0701fc1 --- /dev/null +++ b/tests/unit/test_deprecated_imports.py @@ -0,0 +1,69 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import os +import re + +from ansible_collections.ansible.posix.tests.unit.compat import unittest + + +# Deprecated import patterns that trigger warnings on ansible-core >= 2.20 +# and will be removed in ansible-core 2.24. +DEPRECATED_PATTERNS = [ + ( + re.compile(r'from ansible\.module_utils\._text import'), + 'from ansible.module_utils.common.text.converters import', + ), + ( + re.compile(r'from ansible\.module_utils\.common\._collections_compat import'), + 'from collections.abc import', + ), +] + + +def _get_collection_root(): + """Return the root directory of this collection.""" + # tests/unit/test_deprecated_imports.py -> collection root + return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + + +def _find_python_files(root): + """Yield all .py files under root.""" + for dirpath, _dirnames, filenames in os.walk(root): + for filename in filenames: + if filename.endswith('.py'): + yield os.path.join(dirpath, filename) + + +class TestNoDeprecatedImports(unittest.TestCase): + + def test_no_deprecated_module_utils_imports(self): + """Ensure no files use deprecated ansible.module_utils import paths. + + ansible-core 2.20 deprecated ``ansible.module_utils._text`` (use + ``ansible.module_utils.common.text.converters``) and + ``ansible.module_utils.common._collections_compat`` (use + ``collections.abc``). Both will be removed in ansible-core 2.24. + """ + root = _get_collection_root() + violations = [] + + for filepath in _find_python_files(root): + relpath = os.path.relpath(filepath, root) + with open(filepath, 'r') as f: + for lineno, line in enumerate(f, 1): + for pattern, replacement in DEPRECATED_PATTERNS: + if pattern.search(line): + violations.append( + '%s:%d: deprecated import found:\n' + ' %s\n' + ' replace with: %s' + % (relpath, lineno, line.rstrip(), replacement) + ) + + if violations: + self.fail( + 'Found deprecated imports that will break on ansible-core 2.24:\n\n' + + '\n\n'.join(violations) + )