Coverage for source/utils/dynamic_from_string_converter.py: 98%

42 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-08-01 20:51 +0000

1# utils/dynamic_from_string_converter.py 

2 

3# global imports 

4import importlib 

5import logging 

6import pkgutil 

7from anytree import Node 

8from typing import Optional 

9from types import ModuleType 

10 

11# local imports 

12from source.utils import SingletonMeta 

13 

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 """ 

19 

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. 

23 

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 """ 

29 

30 super().__init__(name, parent) 

31 self.package: ModuleType = package 

32 

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 """ 

39 

40 def __init__(self) -> None: 

41 """ 

42 Class constructor. Initializes an empty list to hold registered packages. 

43 """ 

44 

45 self.__registered_packages: list[PackageNode] = [] 

46 

47 def __build_package_tree(self, package_name: str, parent: Optional[PackageNode] = None) -> PackageNode: 

48 """ 

49 Builds a tree structure of packages starting from the given package name. 

50 

51 Parameters: 

52 package_name (str): The name of the package to build the tree for. 

53 parent (Optional[PackageNode]): The parent node in the tree structure. 

54 

55 Returns: 

56 (PackageNode): The root node of the package tree. 

57 """ 

58 

59 root_node: Optional[PackageNode] = None 

60 try: 

61 package = importlib.import_module(package_name) 

62 root_node = PackageNode(name = package_name, package = package, parent = parent) 

63 

64 if hasattr(package, '__path__'): 

65 for _, module_name, is_pkg in pkgutil.iter_modules(package.__path__, package_name + '.'): 

66 if is_pkg: 

67 self.__build_package_tree(module_name, parent = root_node) 

68 except ImportError as e: 

69 logging.error(f"Failed to import {package_name}: {e}") 

70 

71 return root_node 

72 

73 def __find_class_in_package_tree(self, class_name: str, package_node: PackageNode) -> Optional[type]: 

74 """ 

75 Searches for a class in the package tree. 

76 

77 Parameters: 

78 class_name (str): The name of the class to search for. 

79 package_node (PackageNode): The root node of the package tree to search. 

80 

81 Returns: 

82 (Optional[type]): The class type if found, None otherwise. 

83 """ 

84 

85 if hasattr(package_node.package, class_name): 

86 return getattr(package_node.package, class_name) 

87 

88 for child in package_node.children: 

89 found_class = self.__find_class_in_package_tree(class_name, child) 

90 if found_class is not None: 

91 return found_class 

92 

93 return None 

94 

95 

96 def register_packages(self, packages_to_get_registered: list[str]) -> None: 

97 """ 

98 Registers a list of packages by building their package trees. 

99 

100 Parameters: 

101 packages_to_get_registered (list[str]): A list of package names to register. 

102 """ 

103 

104 self.__registered_packages: list[PackageNode] = [self.__build_package_tree(package_name) \ 

105 for package_name in packages_to_get_registered] 

106 

107 def get_class_handle(self, class_name: str) -> type: 

108 """ 

109 Retrieves the class handle for a given class name from the registered packages. 

110 

111 Parameters: 

112 class_name (str): The name of the class to retrieve. 

113 

114 Raises: 

115 ValueError: If the class is not found in the registered packages. 

116 

117 Returns: 

118 (type): The class type if found. 

119 """ 

120 

121 for package_tree in self.__registered_packages: 

122 found_class = self.__find_class_in_package_tree(class_name, package_tree) 

123 if found_class is not None: 

124 return found_class 

125 

126 raise ValueError(f"Class '{class_name}' not found in registered packages.")