1
+ import importlib .util
2
+ import inspect
3
+ from pathlib import Path
4
+ from typing import Any , Iterable , Type , Optional
5
+
6
+ from loguru import logger
7
+
8
+
9
+ class ModuleImporter :
10
+ """
11
+ A class that handles dynamic module importing with caching capabilities.
12
+ Provides functionality to import modules from file paths and extract classes from them.
13
+ """
14
+
15
+ def __init__ (self ) -> None :
16
+ # Module cache to prevent duplicate imports
17
+ self ._module_cache : dict [str , Any ] = {}
18
+
19
+ def import_module_from_path (self , file_path : str ) -> Optional [Any ]:
20
+ """
21
+ Import a module from a file path with caching.
22
+
23
+ Args:
24
+ file_path (str): The file path of the module to import.
25
+
26
+ Returns:
27
+ Optional[Any]: The imported module or None if import fails.
28
+ """
29
+ resolved_path = Path (file_path ).resolve ()
30
+ cache_key = str (resolved_path )
31
+ module_name = resolved_path .stem
32
+
33
+ # Check if module is already cached
34
+ if cache_key in self ._module_cache :
35
+ logger .debug (f"[MODULE CACHE] Using cached module: { module_name } " )
36
+ return self ._module_cache [cache_key ]
37
+
38
+ logger .info (f"[MODULE IMPORT] Import module path: { resolved_path } " )
39
+
40
+ # Create a module specification
41
+ spec = importlib .util .spec_from_file_location (module_name , resolved_path )
42
+ if spec is None :
43
+ logger .warning (f"[MODULE IMPORT] Could not create spec for { module_name } " )
44
+ return None
45
+
46
+ # Create a new module based on the specification
47
+ module = importlib .util .module_from_spec (spec )
48
+ if spec .loader is None :
49
+ logger .warning (f"[MODULE IMPORT] No loader found for { module_name } " )
50
+ return None
51
+
52
+ # Execute the module in its own namespace
53
+ logger .info (f"[MODULE IMPORT] Import module: { module_name } " )
54
+ try :
55
+ spec .loader .exec_module (module )
56
+ logger .success (f"[MODULE IMPORT] Successfully imported { module_name } " )
57
+ # Cache the module
58
+ self ._module_cache [cache_key ] = module
59
+ return module
60
+ except Exception as error :
61
+ logger .warning (f"[MODULE IMPORT] Failed to import { module_name } : { error } " )
62
+ return None
63
+
64
+ def extract_classes_from_module (self , module : Any ) -> list [Type [object ]]:
65
+ """
66
+ Extract all classes from a module.
67
+
68
+ Args:
69
+ module (Any): The module to extract classes from.
70
+
71
+ Returns:
72
+ list[Type[object]]: List of classes found in the module.
73
+ """
74
+ loaded_classes = []
75
+ for attr in dir (module ):
76
+ obj = getattr (module , attr )
77
+ if attr .startswith ("__" ):
78
+ continue
79
+ if not inspect .isclass (obj ):
80
+ continue
81
+ loaded_classes .append (obj )
82
+ return loaded_classes
83
+
84
+ def import_classes_from_paths (
85
+ self ,
86
+ file_paths : Iterable [str ],
87
+ target_subclasses : Iterable [Type [object ]] = [],
88
+ ignore_errors : bool = True
89
+ ) -> set [Type [object ]]:
90
+ """
91
+ Import classes from multiple file paths with optional filtering.
92
+
93
+ Args:
94
+ file_paths (Iterable[str]): The file paths of the modules to import.
95
+ target_subclasses (Iterable[Type[object]], optional): Target subclasses to filter. Defaults to [].
96
+ ignore_errors (bool, optional): Whether to ignore import errors. Defaults to True.
97
+
98
+ Returns:
99
+ set[Type[object]]: Set of imported classes.
100
+ """
101
+ all_loaded_classes : list [Type [object ]] = []
102
+
103
+ for file_path in file_paths :
104
+ module = self .import_module_from_path (file_path )
105
+ if module is None :
106
+ if not ignore_errors :
107
+ raise ImportError (f"Failed to import module from { file_path } " )
108
+ continue
109
+
110
+ loaded_classes = self .extract_classes_from_module (module )
111
+ all_loaded_classes .extend (loaded_classes )
112
+
113
+ returned_target_classes : set [Type [object ]] = set ()
114
+
115
+ # If no target subclasses specified, return all loaded classes
116
+ if not target_subclasses :
117
+ returned_target_classes = set (all_loaded_classes )
118
+ else :
119
+ # Filter classes based on target subclasses
120
+ for target_cls in target_subclasses :
121
+ for loaded_class in all_loaded_classes :
122
+ if loaded_class in target_subclasses :
123
+ continue
124
+ if issubclass (loaded_class , target_cls ):
125
+ returned_target_classes .add (loaded_class )
126
+
127
+ return returned_target_classes
128
+
129
+ def clear_cache (self ) -> None :
130
+ """Clear the module cache. Useful for testing or when you need to force re-import."""
131
+ self ._module_cache .clear ()
132
+
133
+ def get_cache_size (self ) -> int :
134
+ """Get the number of cached modules."""
135
+ return len (self ._module_cache )
0 commit comments