Module trappedbot.tasks.dynload

Dynamically load a module/package from an arbitrary location

Expand source code
"""Dynamically load a module/package from an arbitrary location"""

import importlib.util
import os
import sys
import types
import typing

from importlib.abc import Loader

from trappedbot.applogger import LOGGER


class DynloadSpecError(BaseException):
    pass


class DynloadDuplicateModuleError(BaseException):
    pass


def dynamically_load_module(name: str, path: str) -> types.ModuleType:
    """Load a module or package from a dynamic location

    This allows users to write custom Python code for bot tasks

    name:   A name for the module.
            Take care that this is GLOBALLY UNIQUE for this Python process.
    path:   The path to the module.

    It will return the module to the caller.
    To use it, the caller must save the result and call it that way;
    it will not be usable via standard Python 'import'.
    (If you want that, install the module as a Python package to the Python path
    before starting Python.)
    """

    if name in sys.modules:
        raise DynloadDuplicateModuleError(name)

    # If the user passes a directory, assume it is a Python package
    module_path = path if not os.path.isdir(path) else os.path.join(path, "__init__.py")

    spec = importlib.util.spec_from_file_location(name, module_path)
    if not isinstance(spec.loader, Loader):
        raise DynloadSpecError(name, path)

    dynmod = importlib.util.module_from_spec(spec)
    sys.modules[name] = dynmod
    spec.loader.exec_module(dynmod)

    return dynmod


def trappedbot_dynload_for_taskfunc(
    name: str, path: str
) -> typing.Optional[types.FunctionType]:
    """Dynamically load a module and return a trappedbot taskfunc

    The taskfunc must be named 'trappedbot_task' exactly,
    and it must have the TaskFunc signature.
    """
    dynmod_name = f"trappedbot_extension_{name}"

    try:
        dynmod = dynamically_load_module(dynmod_name, path)
    except DynloadSpecError:
        LOGGER.error(
            f"Could not dynamically load a module called {dynmod_name} from {path} because it could not find a Python module or package at that location."
        )
        return None
    except DynloadDuplicateModuleError:
        LOGGER.error(
            f"Could not dynamically load a module called {dynmod_name} from {path} because a module with that name already exists."
        )
        return None

    # Ignore type checking on the dynamic module, but log an error if it doesn't have a trappedbot_task
    try:
        taskfunc = dynmod.trappedbot_task  # type: ignore
    except AttributeError:
        LOGGER.error(
            f"Cannot use dynamically loaded module called {dynmod_name} from {path} because it does not export a 'trappedbot_task' function"
        )
        return None

    return taskfunc

Functions

def dynamically_load_module(name: str, path: str) ‑> module

Load a module or package from a dynamic location

This allows users to write custom Python code for bot tasks

name: A name for the module. Take care that this is GLOBALLY UNIQUE for this Python process. path: The path to the module.

It will return the module to the caller. To use it, the caller must save the result and call it that way; it will not be usable via standard Python 'import'. (If you want that, install the module as a Python package to the Python path before starting Python.)

Expand source code
def dynamically_load_module(name: str, path: str) -> types.ModuleType:
    """Load a module or package from a dynamic location

    This allows users to write custom Python code for bot tasks

    name:   A name for the module.
            Take care that this is GLOBALLY UNIQUE for this Python process.
    path:   The path to the module.

    It will return the module to the caller.
    To use it, the caller must save the result and call it that way;
    it will not be usable via standard Python 'import'.
    (If you want that, install the module as a Python package to the Python path
    before starting Python.)
    """

    if name in sys.modules:
        raise DynloadDuplicateModuleError(name)

    # If the user passes a directory, assume it is a Python package
    module_path = path if not os.path.isdir(path) else os.path.join(path, "__init__.py")

    spec = importlib.util.spec_from_file_location(name, module_path)
    if not isinstance(spec.loader, Loader):
        raise DynloadSpecError(name, path)

    dynmod = importlib.util.module_from_spec(spec)
    sys.modules[name] = dynmod
    spec.loader.exec_module(dynmod)

    return dynmod
def trappedbot_dynload_for_taskfunc(name: str, path: str) ‑> Optional[function]

Dynamically load a module and return a trappedbot taskfunc

The taskfunc must be named 'trappedbot_task' exactly, and it must have the TaskFunc signature.

Expand source code
def trappedbot_dynload_for_taskfunc(
    name: str, path: str
) -> typing.Optional[types.FunctionType]:
    """Dynamically load a module and return a trappedbot taskfunc

    The taskfunc must be named 'trappedbot_task' exactly,
    and it must have the TaskFunc signature.
    """
    dynmod_name = f"trappedbot_extension_{name}"

    try:
        dynmod = dynamically_load_module(dynmod_name, path)
    except DynloadSpecError:
        LOGGER.error(
            f"Could not dynamically load a module called {dynmod_name} from {path} because it could not find a Python module or package at that location."
        )
        return None
    except DynloadDuplicateModuleError:
        LOGGER.error(
            f"Could not dynamically load a module called {dynmod_name} from {path} because a module with that name already exists."
        )
        return None

    # Ignore type checking on the dynamic module, but log an error if it doesn't have a trappedbot_task
    try:
        taskfunc = dynmod.trappedbot_task  # type: ignore
    except AttributeError:
        LOGGER.error(
            f"Cannot use dynamically loaded module called {dynmod_name} from {path} because it does not export a 'trappedbot_task' function"
        )
        return None

    return taskfunc

Classes

class DynloadDuplicateModuleError (*args, **kwargs)

Common base class for all exceptions

Expand source code
class DynloadDuplicateModuleError(BaseException):
    pass

Ancestors

  • builtins.BaseException
class DynloadSpecError (*args, **kwargs)

Common base class for all exceptions

Expand source code
class DynloadSpecError(BaseException):
    pass

Ancestors

  • builtins.BaseException