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

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, 

48 extended: bool = False) -> PackageNode: 

49 """ 

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

51 

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. 

56 

57 Returns: 

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

59 """ 

60 

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) 

65 

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

72 

73 return root_node 

74 

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. 

78 

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. 

82 

83 Returns: 

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

85 """ 

86 

87 if hasattr(package_node.package, class_name): 

88 return getattr(package_node.package, class_name) 

89 

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 

94 

95 return None 

96 

97 

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. 

101 

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

106 

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()] 

109 

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. 

113 

114 Parameters: 

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

116 

117 Raises: 

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

119 

120 Returns: 

121 (type): The class type if found. 

122 """ 

123 

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 

128 

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