From fd07a94b6e6e80136d3b1ac4f9865e2f868b64a4 Mon Sep 17 00:00:00 2001 From: Thorben Nissen Date: Wed, 25 Mar 2026 11:37:13 +0100 Subject: [PATCH 1/3] fix: Make relative imports in collection modules work Use `NewStylePlanner` for collection modules with relative imports and replace the relative imports with absolute imports. --- ansible_mitogen/planner.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/ansible_mitogen/planner.py b/ansible_mitogen/planner.py index 5d11de3e3..6dd0283d3 100644 --- a/ansible_mitogen/planner.py +++ b/ansible_mitogen/planner.py @@ -317,7 +317,7 @@ class NewStylePlanner(ScriptPlanner): preprocessing the module. """ runner_name = 'NewStyleRunner' - MARKER = re.compile(br'from ansible(?:_collections|\.module_utils)\.') + MARKER = re.compile(br'from ansible(?:_collections|\.module_utils)\.|from \.\.?(?:\w|\s)') @classmethod def detect(cls, path, source): @@ -653,6 +653,40 @@ def _fix_dnf(invocation, module_source): invocation._overridden_sources[invocation.module_path] = module_source +def _fix_collection_relative_imports(invocation, module_source): + """ + Collection modules using relative imports (from ..module_utils import ...) + fail because the ansible_collections namespace isn't set up. + Replace relative imports with absolute ansible_collections imports. + """ + if 'ansible_collections' not in invocation.module_path: + return + + # Derive the collection's base package from the module path + # e.g. .../ansible_collections/hetzner/hcloud/plugins/modules/foo.py + # -> ansible_collections.hetzner.hcloud.plugins + parts = invocation.module_path.split(os.sep) + try: + ac_idx = parts.index('ansible_collections') + except ValueError: + return + + # namespace.collection.plugins + if len(parts) < ac_idx + 4: + return + + ns, coll, _, plugins_dir = parts[ac_idx + 1], parts[ac_idx + 2], parts[ac_idx + 3], parts[ac_idx + 3] + abs_prefix = 'ansible_collections.%s.%s.plugins.module_utils' % (ns, coll) + + # Replace `from ..module_utils` with the absolute collection path + fixed = module_source.replace( + b'from ..module_utils', + ('from %s' % abs_prefix).encode() + ) + if fixed != module_source: + invocation._overridden_sources[invocation.module_path] = fixed + + def _load_collections(invocation): """ Special loader that ensures that `ansible_collections` exist as a module path for import @@ -691,6 +725,7 @@ def invoke(invocation): module_source = invocation.get_module_source() _fix_py35(invocation, module_source) _fix_dnf(invocation, module_source) + _fix_collection_relative_imports(invocation, module_source) _planner_by_path[invocation.module_path] = _get_planner( invocation, module_source From efa6de7732127981bc5134c8f4919bbc955ed711 Mon Sep 17 00:00:00 2001 From: Thorben Nissen Date: Thu, 26 Mar 2026 13:09:11 +0100 Subject: [PATCH 2/3] fix: Make relative imports in collection modules work Replace dots with correct module path when resolving relative imports. --- ansible_mitogen/planner.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/ansible_mitogen/planner.py b/ansible_mitogen/planner.py index 6dd0283d3..da3760ff9 100644 --- a/ansible_mitogen/planner.py +++ b/ansible_mitogen/planner.py @@ -671,18 +671,31 @@ def _fix_collection_relative_imports(invocation, module_source): except ValueError: return - # namespace.collection.plugins - if len(parts) < ac_idx + 4: + # Need at least ansible_collections/{namespace}/{collection} + if len(parts) < ac_idx + 3: return - ns, coll, _, plugins_dir = parts[ac_idx + 1], parts[ac_idx + 2], parts[ac_idx + 3], parts[ac_idx + 3] - abs_prefix = 'ansible_collections.%s.%s.plugins.module_utils' % (ns, coll) + module_package_parts = parts[ac_idx:parts.index(parts[-1])] + module_package = '.'.join(module_package_parts) + + def resolve_relative_import(match): + dots = match.group(1) + package_name = match.group(2) + level = len(dots) + + base_parts = module_package.split('.') + base_parts = base_parts[:len(base_parts) - level + 1] + + if package_name: + absolute_package_name = ('from %s.%s' % ('.'.join(base_parts), package_name.decode())).encode() + else: + absolute_package_name = ('from %s' % '.'.join(base_parts)).encode() + + return absolute_package_name + + pattern = re.compile(rb'from (\.\.?)\s?(\w+)') + fixed = pattern.sub(resolve_relative_import, module_source) - # Replace `from ..module_utils` with the absolute collection path - fixed = module_source.replace( - b'from ..module_utils', - ('from %s' % abs_prefix).encode() - ) if fixed != module_source: invocation._overridden_sources[invocation.module_path] = fixed From 317c3b71b48c5b87f8807bf3ab08c8024f379e03 Mon Sep 17 00:00:00 2001 From: Thorben Nissen Date: Thu, 26 Mar 2026 13:19:58 +0100 Subject: [PATCH 3/3] fix: Make relative imports in collection modules work The `r` prefix for regular expressions is not supported in Python 2.x --- ansible_mitogen/planner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible_mitogen/planner.py b/ansible_mitogen/planner.py index da3760ff9..f99ff760f 100644 --- a/ansible_mitogen/planner.py +++ b/ansible_mitogen/planner.py @@ -693,7 +693,7 @@ def resolve_relative_import(match): return absolute_package_name - pattern = re.compile(rb'from (\.\.?)\s?(\w+)') + pattern = re.compile(b'from (\.\.?)\s?(\w+)') fixed = pattern.sub(resolve_relative_import, module_source) if fixed != module_source: