How to use module_utils in Event Driven Ansible (eda) plugins

I am trying to write an Event Driven Ansible plugin that uses module_utils from the same collection. I cannot get the imports working properly, so I was wondering if anyone had any idea on how to do this or what to try.

Here is my current work in progress: check in changes by mikemorency · Pull Request #494 · ansible-collections/servicenow.itsm · GitHub

I’ve tried the following imports:

ansible_collections.servicenow.itsm.plugins.module_utils...
plugins.module_utils....
.......plugins.module_utils....

But they dont seem to work, even if I try to run the plugin using ansible-rulebook

You are trying an absolute import, but you want to load a local module, you can try a relative import, as it are already doing the modules, for example: servicenow.itsm/plugins/modules/api.py at main · ansible-collections/servicenow.itsm · GitHub

Hi,

I changed my imports to this:

from .....plugins.module_utils.instance_config import get_combined_instance_config
from .....plugins.module_utils import client, table

And then ansible rulebook throws this error:

> ansible-galaxy collection install . --force; ansible-rulebook -E SN_HOST,SN_USERNAME,SN_PASSWORD -v -r test.yml


....
2025-08-12 08:57:14,129 - ansible_rulebook.app - ERROR - ImportError: attempted relative import with no known parent package
2025-08-12 08:57:14,129 - ansible_rulebook.app - ERROR - /home/mimorenc/.ansible/collections/ansible_collections/servicenow/itsm/extensions/eda/plugins/event_source/records.py:67 <module>
2025-08-12 08:57:14,130 - ansible_rulebook.app - ERROR - <frozen runpy>:88 _run_code
2025-08-12 08:57:14,130 - ansible_rulebook.app - ERROR - <frozen runpy>:98 _run_module_code
2025-08-12 08:57:14,130 - ansible_rulebook.app - ERROR - <frozen runpy>:291 run_path
2025-08-12 08:57:14,130 - ansible_rulebook.app - ERROR - /home/mimorenc/miniconda3/envs/test/lib/python3.11/site-packages/ansible_rulebook/engine.py:120 start_source
2025-08-12 08:57:14,130 - ansible_rulebook.app - INFO - Main complete
2025-08-12 08:57:14,130 - ansible_rulebook.cli - ERROR - Terminating: One of the source plugins failed

This looks like a very nested relative import which might not be a good idea in terms of readability.
The error “ImportError: attempted relative import with no known parent package”
means the parent package is not actually a package, probably it is lacking __init__.py files. ANy package needs this file to be recognized as package by python.

To avoid relative imports, you should be able to access the package by its full absolute package. You mentioned that ansible_collections.servicenow.itsm.plugins.module_utils didn’t work, but ansible-collections does not seem to be a package, just a github namespace.
I think from servicenow.itsm.plugins.module_utils.instance_config import get_combined_instance_config should work.
The problem that I see is that servicenow.itsm is interpreted as [pyproject]/itsm.
I think the dot in the package name is problematic, if it does not work, you will might need to load the module explicitly with importlib

I tried these without luck:

# from .....plugins.module_utils.instance_config import get_combined_instance_config
# from .....plugins.module_utils import client, table

# from servicenow.itsm.plugins.module_utils.instance_config import get_combined_instance_config
# from servicenow.itsm.plugins.module_utils import client, table

# from ansible_collections.servicenow.itsm.plugins.module_utils.instance_config import get_combined_instance_config
# from ansible_collections.servicenow.itsm.plugins.module_utils import client, table

Ill give the importlib approach a try and report back

Another thing that I see is that you are placing a new instance_config module in plugins/module_utils that is out of the scope of extensions/eda/plugins.
/plugins/module_utils is intended for ansible-core while /extensions/eda/plugins is intended for ansible-rulebook.

Since this module is to be used by the source plugin, I suggest to locate in extensions/eda/plugins/utils, then you can use a relative import like ..utils and that should work and probably would be a more readable code. (don’t forget to add an empty __init__.py file so python can recognize it as package)

Unfortunately that creates a lot of duplicated code in our repository. The intent of using stuff from module_utils is to use code that already exists for the modules or other plugins. The instance_config module specifically is code that is already in the inventory plugin but can be used by the EDA plugins. So I am moving it to a shared location, and then will update the inventory plugin to use that instead of local methods

Ok, I just saw a new instance_config module in the PR and I thought that would be just for the eda plugin. For more reusability as you mention, my last comment is not a good idea, the explicit import with importlib to use on demand whatever you want for the rest of the collection seems to make more sense.

I tried using importlib without success. However, their docs suggest using sys.path instead, which is what I tried as a hack/stopgap so I will likely go with that.

For import lib i tried:

import pathlib
import importlib.util
import sys

def import_from_path(module_name, file_path):
    spec = importlib.util.spec_from_file_location(module_name, file_path)
    module = importlib.util.module_from_spec(spec)
    sys.modules[module_name] = module
    spec.loader.exec_module(module)
    return module

CURRENT_EDA_PLUGIN_PATH = pathlib.Path(__file__).parent.resolve()
print(f"CURRENT_EDA_PLUGIN_PATH: {CURRENT_EDA_PLUGIN_PATH}")
MODULE_UTILS_PATH = f"{CURRENT_EDA_PLUGIN_PATH.parent.parent.parent.parent}/plugins/module_utils"
get_combined_instance_config = import_from_path("get_combined_instance_config", f"{MODULE_UTILS_PATH}/instance_config.py")
Client = import_from_path("Client", f"{MODULE_UTILS_PATH}/client.py")
TableClient = import_from_path("TableClient", f"{MODULE_UTILS_PATH}/table.py")

But kept getting ImportError: attempted relative import with no known parent package

The working approach with sys.path is

import sys
import os
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../..'))
if project_root not in sys.path:
    sys.path.insert(0, project_root)

from plugins.module_utils.instance_config import get_combined_instance_config
from plugins.module_utils import client, table