Skip to content

kiara.utils.class_loading

find_all_kiara_modules()

Find all KiaraModule subclasses via package entry points.

TODO

Source code in kiara/utils/class_loading.py
def find_all_kiara_modules() -> typing.Dict[str, typing.Type["KiaraModule"]]:
    """Find all [KiaraModule][kiara.module.KiaraModule] subclasses via package entry points.

    TODO
    """

    from kiara.module import KiaraModule

    modules = load_all_subclasses_for_entry_point(
        entry_point_name="kiara.modules",
        base_class=KiaraModule,  # type: ignore
        set_id_attribute="_module_type_name",
        remove_namespace_tokens=["core."],
    )
    result = {}
    # need to test this, since I couldn't add an abstract method to the KiaraModule class itself (mypy complained because it is potentially overloaded)
    for k, cls in modules.items():

        if not hasattr(cls, "process"):
            msg = f"Ignoring module class '{cls}': no 'process' method."
            if is_debug():
                log.warning(msg)
            else:
                log.debug(msg)
            continue

        # TODO: check signature of process method

        if k.startswith("_"):
            tokens = k.split(".")
            if len(tokens) == 1:
                k = k[1:]
            else:
                k = ".".join(tokens[1:])

        result[k] = cls
    return result

find_all_metadata_models()

Find all KiaraModule subclasses via package entry points.

TODO

Source code in kiara/utils/class_loading.py
def find_all_metadata_models() -> typing.Dict[str, typing.Type["MetadataModel"]]:
    """Find all [KiaraModule][kiara.module.KiaraModule] subclasses via package entry points.

    TODO
    """

    return load_all_subclasses_for_entry_point(
        entry_point_name="kiara.metadata_models",
        base_class=MetadataModel,
        set_id_attribute="_metadata_key",
        remove_namespace_tokens=["core."],
    )

find_all_value_types()

Find all KiaraModule subclasses via package entry points.

TODO

Source code in kiara/utils/class_loading.py
def find_all_value_types() -> typing.Dict[str, typing.Type["ValueType"]]:
    """Find all [KiaraModule][kiara.module.KiaraModule] subclasses via package entry points.

    TODO
    """

    all_value_types = load_all_subclasses_for_entry_point(
        entry_point_name="kiara.value_types",
        base_class=ValueType,  # type: ignore
        set_id_attribute="_value_type_name",
        remove_namespace_tokens=True,
    )

    invalid = [x for x in all_value_types.keys() if "." in x]
    if invalid:
        raise Exception(
            f"Invalid value type name(s), type names can't contain '.': {', '.join(invalid)}"
        )

    return all_value_types

find_subclasses_under(base_class, module, prefix='', remove_namespace_tokens=None, module_name_func=None)

Find all (non-abstract) subclasses of a base class that live under a module (recursively).

Parameters:

Name Type Description Default
base_class Type[~SUBCLASS_TYPE]

the parent class

required
module Union[str, module]

the module to search

required
prefix Optional[str]

a string to use as a result items namespace prefix, defaults to an empty string, use 'None' to indicate the module path should be used

''
remove_namespace_tokens Optional[Iterable[str]]

a list of strings to remove from module names when autogenerating subclass ids, and prefix is None

None

Returns:

Type Description
Mapping[str, Type[~SUBCLASS_TYPE]]

a map containing the (fully namespaced) id of the subclass as key, and the actual class object as value

Source code in kiara/utils/class_loading.py
def find_subclasses_under(
    base_class: typing.Type[SUBCLASS_TYPE],
    module: typing.Union[str, ModuleType],
    prefix: typing.Optional[str] = "",
    remove_namespace_tokens: typing.Optional[typing.Iterable[str]] = None,
    module_name_func: typing.Callable = None,
) -> typing.Mapping[str, typing.Type[SUBCLASS_TYPE]]:
    """Find all (non-abstract) subclasses of a base class that live under a module (recursively).

    Arguments:
        base_class: the parent class
        module: the module to search
        prefix: a string to use as a result items namespace prefix, defaults to an empty string, use 'None' to indicate the module path should be used
        remove_namespace_tokens: a list of strings to remove from module names when autogenerating subclass ids, and prefix is None

    Returns:
        a map containing the (fully namespaced) id of the subclass as key, and the actual class object as value
    """

    if hasattr(sys, "frozen"):
        raise NotImplementedError("Pyinstaller bundling not supported yet.")

    if isinstance(module, str):
        module = importlib.import_module(module)

    _import_modules_recursively(module)

    subclasses: typing.Iterable[typing.Type[SUBCLASS_TYPE]] = _get_all_subclasses(
        base_class
    )

    result = {}
    for sc in subclasses:

        if not sc.__module__.startswith(module.__name__):
            continue

        if inspect.isabstract(sc):
            if is_debug():
                # import traceback
                # traceback.print_stack()
                log.warning(f"Ignoring abstract subclass: {sc}")
            else:
                log.debug(f"Ignoring abstract subclass: {sc}")
            continue

        if module_name_func is None:
            module_name_func = _get_subclass_name
        name = module_name_func(sc)
        path = sc.__module__[len(module.__name__) + 1 :]  # noqa

        if path:
            full_name = f"{path}.{name}"
        else:
            full_name = name

        if prefix is None:
            prefix = module.__name__ + "."
            if remove_namespace_tokens:
                for rnt in remove_namespace_tokens:
                    if prefix.startswith(rnt):
                        prefix = prefix[0 : -len(rnt)]  # noqa

        if prefix:
            full_name = f"{prefix}.{full_name}"

        result[full_name] = sc

    return result

load_all_subclasses_for_entry_point(entry_point_name, base_class, set_id_attribute=None, remove_namespace_tokens=None)

Find all subclasses of a base class via package entry points.

Parameters:

Name Type Description Default
entry_point_name str

the entry point name to query entries for

required
base_class Type[~SUBCLASS_TYPE]

the base class to look for

required
set_id_attribute Optional[str]

whether to set the entry point id as attribute to the class, if None, no id attribute will be set, if a string, the attribute with that name will be set

None
remove_namespace_tokens Union[Iterable[str], bool]

a list of strings to remove from module names when autogenerating subclass ids, and prefix is None, or a boolean in which case all or none namespaces will be removed

None

TODO

Source code in kiara/utils/class_loading.py
def load_all_subclasses_for_entry_point(
    entry_point_name: str,
    base_class: typing.Type[SUBCLASS_TYPE],
    set_id_attribute: typing.Union[None, str] = None,
    remove_namespace_tokens: typing.Union[typing.Iterable[str], bool, None] = None,
) -> typing.Dict[str, typing.Type[SUBCLASS_TYPE]]:
    """Find all subclasses of a base class via package entry points.

    Arguments:
        entry_point_name: the entry point name to query entries for
        base_class: the base class to look for
        set_id_attribute: whether to set the entry point id as attribute to the class, if None, no id attribute will be set, if a string, the attribute with that name will be set
        remove_namespace_tokens: a list of strings to remove from module names when autogenerating subclass ids, and prefix is None, or a boolean in which case all or none namespaces will be removed

    TODO
    """

    log2 = logging.getLogger("stevedore")
    out_hdlr = logging.StreamHandler(sys.stdout)
    out_hdlr.setFormatter(
        logging.Formatter(f"{entry_point_name} plugin search error -> %(message)s")
    )
    out_hdlr.setLevel(logging.INFO)
    log2.addHandler(out_hdlr)
    log2.setLevel(logging.INFO)

    log.debug(f"Finding {entry_point_name} items from search paths...")

    mgr = ExtensionManager(
        namespace=entry_point_name,
        invoke_on_load=False,
        propagate_map_exceptions=True,
    )

    result_entrypoints: typing.Dict[str, typing.Type] = {}
    result_dynamic: typing.Dict[str, typing.Type] = {}
    for plugin in mgr:
        name = plugin.name

        if isinstance(plugin.plugin, type) and issubclass(plugin.plugin, base_class):
            ep = plugin.entry_point
            module_cls = ep.load()

            if set_id_attribute:
                if hasattr(module_cls, set_id_attribute):
                    if not getattr(module_cls, set_id_attribute) == name:
                        log.warning(
                            f"Item id mismatch for type {entry_point_name}: {getattr(module_cls, set_id_attribute)} != {name}, entry point key takes precedence: {name})"
                        )
                        setattr(module_cls, set_id_attribute, name)

                else:
                    setattr(module_cls, set_id_attribute, name)
            result_entrypoints[name] = module_cls
        elif (
            isinstance(plugin.plugin, tuple)
            and len(plugin.plugin) >= 1
            and callable(plugin.plugin[0])
        ) or callable(plugin.plugin):
            modules = _callable_wrapper(plugin.plugin)

            for k, v in modules.items():
                _name = f"{name}.{k}"
                if _name in result_dynamic.keys():
                    raise Exception(
                        f"Duplicate item name for type {entry_point_name}: {_name}"
                    )
                result_dynamic[_name] = v

        else:
            raise Exception(
                f"Can't load subclasses for entry point {entry_point_name} and base class {base_class}: invalid plugin type {type(plugin.plugin)}"
            )

    for k, v in result_dynamic.items():
        if k in result_entrypoints.keys():
            raise Exception(f"Duplicate item name for type {entry_point_name}: {k}")
        result_entrypoints[k] = v

    result: typing.Dict[str, typing.Type[SUBCLASS_TYPE]] = {}
    for k, v in result_entrypoints.items():

        if remove_namespace_tokens:
            if remove_namespace_tokens is True:
                k = k.split(".")[-1]
            elif isinstance(remove_namespace_tokens, typing.Iterable):
                for rnt in remove_namespace_tokens:
                    if k.startswith(rnt):
                        k = k[len(rnt) :]  # noqa

        if k in result.keys():
            msg = ""
            if set_id_attribute:
                msg = f" Check whether '{v.__name__}' is missing the '{set_id_attribute}' class attribute (in case this is a sub-class), or it's '{k}' value is also set in another class?"
            raise Exception(
                f"Duplicate item name for base class {base_class}: {k}.{msg}"
            )
        result[k] = v

    return result