Coverage for source/utils/dynamic_from_string_converter.py: 98%
42 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-29 20:04 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-09-29 20:04 +0000
1# utils/dynamic_from_string_converter.py
3# global imports
4import importlib
5import logging
6import pkgutil
7from anytree import Node
8from typing import Optional
9from types import ModuleType
11# local imports
12from source.utils import SingletonMeta
14class PackageNode(Node):
15 """
16 Implements a node in the package tree structure. Each node represents a package
17 and contains its name, the package itself, and a reference to its parent node.
18 """
20 def __init__(self, name: str, package: ModuleType, parent: Optional['PackageNode'] = None) -> None:
21 """
22 Class constructor. Initializes the PackageNode with a name, package, and optional parent node.
24 Parameters:
25 name (str): The name of the package.
26 package (ModuleType): The package module.
27 parent (Optional[PackageNode]): The parent node in the tree structure.
28 """
30 super().__init__(name, parent)
31 self.package: ModuleType = package
33class DynamicFromStringConverter(metaclass = SingletonMeta):
34 """
35 Implements a dynamic class converter that allows for the conversion of class names
36 from strings to actual class handles. It builds a tree structure of packages and allows
37 for the retrieval of class handles based on their names.
38 """
40 def __init__(self) -> None:
41 """
42 Class constructor. Initializes an empty list to hold registered packages.
43 """
45 self.__registered_packages: list[PackageNode] = []
47 def __build_package_tree(self, package_name: str, parent: Optional[PackageNode] = None,
48 extended: bool = False) -> PackageNode:
49 """
50 Builds a tree structure of packages starting from the given package name.
52 Parameters:
53 package_name (str): The name of the package to build the tree for.
54 parent (Optional[PackageNode]): The parent node in the tree structure.
55 extended (bool): Whether to add also modules that are not packages. Defaults to False.
57 Returns:
58 (PackageNode): The root node of the package tree.
59 """
61 root_node: Optional[PackageNode] = None
62 try:
63 package = importlib.import_module(package_name)
64 root_node = PackageNode(name = package_name, package = package, parent = parent)
66 if hasattr(package, '__path__'):
67 for _, module_name, is_package in pkgutil.iter_modules(package.__path__, package_name + '.'):
68 if extended or is_package:
69 self.__build_package_tree(module_name, parent = root_node, extended = extended)
70 except:
71 logging.warning(f"Failed to import {package_name}. Skipping...")
73 return root_node
75 def __find_class_in_package_tree(self, class_name: str, package_node: PackageNode) -> Optional[type]:
76 """
77 Searches for a class in the package tree.
79 Parameters:
80 class_name (str): The name of the class to search for.
81 package_node (PackageNode): The root node of the package tree to search.
83 Returns:
84 (Optional[type]): The class type if found, None otherwise.
85 """
87 if hasattr(package_node.package, class_name):
88 return getattr(package_node.package, class_name)
90 for child in package_node.children:
91 found_class = self.__find_class_in_package_tree(class_name, child)
92 if found_class is not None:
93 return found_class
95 return None
98 def register_packages(self, packages_to_get_registered: dict[str, bool]) -> None:
99 """
100 Registers a list of packages by building their package trees.
102 Parameters:
103 packages_to_get_registered (dict[str, bool]): A dictionary where keys are package names
104 and values indicate whether to include non-package modules (True) or not (False).
105 """
107 self.__registered_packages: list[PackageNode] = [self.__build_package_tree(package_name, extended = extended) \
108 for package_name, extended in packages_to_get_registered.items()]
110 def get_class_handle(self, class_name: str) -> type:
111 """
112 Retrieves the class handle for a given class name from the registered packages.
114 Parameters:
115 class_name (str): The name of the class to retrieve.
117 Raises:
118 ValueError: If the class is not found in the registered packages.
120 Returns:
121 (type): The class type if found.
122 """
124 for package_tree in self.__registered_packages:
125 found_class = self.__find_class_in_package_tree(class_name, package_tree)
126 if found_class is not None:
127 return found_class
129 raise ValueError(f"Class '{class_name}' not found in registered packages.")