# Copyright (c) 2023. OCX Consortium https://3docx.org. See the LICENSE
"""Dynamically load a python module."""
# system imports
import importlib
import sys
from abc import ABC
from types import ModuleType
from typing import Any, List
# 3rd party imports
from loguru import logger
# Project imports
from ocxtools.interfaces.interfaces import IModuleDeclaration
[docs]class ModuleDeclaration(IModuleDeclaration, ABC):
"""General module declaration
Args:
package:the package name
sub_module: The submodule name
name: The method name
"""
def __init__(self, package: str, sub_module: str, name: str):
self._module = sub_module
self._package = package
self._method = name
[docs] def get_declaration(self) -> str:
"""Return the module import declaration."""
return f"{self._package}.{self._module}.{self._method}"
[docs]class DeclarationOfOcxImport(IModuleDeclaration):
"""Declaration of the ocx module."""
def __init__(self, name: str, version: str):
self._name = name
self._version = version
[docs] def get_declaration(self) -> str:
"""Return the module import declaration."""
ocx_pkg = f'{self._name}_{self._version.replace(".", "")}'
return f"{self._name}.{ocx_pkg}.{ocx_pkg}"
[docs] def get_version(self) -> str:
"""Return the OCX module version."""
return self._version
[docs] def get_name(self) -> str:
"""Return the declared module name."""
return self._name
[docs]class DynamicLoader:
"""Dynamically loads modules, classes of functions from a module declaration."""
@classmethod
def _load(cls, declaration: str) -> Any:
"""Internal Method: Import the object from the declaration.
Args:
declaration: The module declaration string.
Returns:
Return the loaded object, None if failed.
"""
obj_type = None
if (spec := importlib.util.find_spec(declaration)) is not None:
obj_type = importlib.util.module_from_spec(spec)
sys.modules[declaration] = obj_type
spec.loader.exec_module(obj_type)
# logger.debug(f"Loaded object {declaration!r} from location {spec.origin!r}")
else:
logger.error(f"No object {declaration!r}")
return obj_type
[docs] @classmethod
def import_module(cls, module_declaration: IModuleDeclaration) -> ModuleType:
"""
Args:
module_declaration: The declaration of the pyton module to load
Returns:
Return the loaded module, None if failed.
"""
module_to_load = module_declaration.get_declaration()
return cls._load(module_to_load)
[docs] @classmethod
def import_class(
cls, module_declaration: IModuleDeclaration, class_name: str
) -> Any:
"""
The module import declaration.
Args:
class_name: The class name to load form the declared module
module_declaration: The declaration of the python module to be loaded
Returns:
Return the loaded class, None if failed.
"""
obj = None
module_to_load = module_declaration.get_declaration()
module = cls._load(module_to_load)
try:
obj = getattr(module, class_name)
# logger.debug(f"Loaded class {class_name!r}")
except AttributeError as e:
raise DynamicLoaderError(
f"No class with name {class_name!r} in module {module_to_load!r}"
) from e
return obj
[docs] @classmethod
def get_all_class_names(cls, module_name: str, version: str) -> List:
"""Return all class names in the module by the ``__all__`` variable.
Args:
module_name: The module name
version: The module version
Returns:
The list of available module class names.
"""
all_names = []
ocx_pkg = f'{module_name}_{version.replace(".", "")}'
ocx_module = f"{module_name}.{ocx_pkg}"
if (spec := importlib.util.find_spec(ocx_module)) is not None:
module = importlib.util.module_from_spec(spec)
sys.modules[ocx_module] = module
spec.loader.exec_module(module)
logger.debug(f"Found module {ocx_module!r} in location {spec.origin}")
all_names = module.__all__
else:
logger.error(f"No module with name {module_name!r} and version {version!r}")
return all_names
[docs]class DynamicLoaderError(AttributeError):
"""Dynamic import errors."""