Coverage for src / mafw / lazy_import.py: 100%

60 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-09 09:08 +0000

1# Copyright 2025 European Union 

2# Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu) 

3# SPDX-License-Identifier: EUPL-1.2 

4""" 

5This module provides classes for lazy importing of plugins, ensuring thread-safe access and transparent usage. 

6 

7Classes: 

8 - LazyImportPlugin: A generic class for lazy importing of plugin classes. 

9 - LazyImportProcessor: A specialised class for lazy importing of Processor plugins. 

10 - LazyImportUserInterface: A specialised class for lazy importing of UserInterface plugins. 

11 

12Protocols: 

13 - ProcessorClassProtocol: Defines the expected interface for Processor classes. 

14 - UserInterfaceClassProtocol: Defines the expected interface for UserInterface classes. 

15 

16.. versionadded:: v2.0.0 

17""" 

18 

19import importlib 

20import threading 

21from abc import ABC, abstractmethod 

22from typing import Any, Generic, Protocol, Type, TypeVar, cast, runtime_checkable 

23 

24from mafw.processor import Processor 

25from mafw.ui.abstract_user_interface import UserInterfaceBase 

26 

27 

28@runtime_checkable 

29class ProcessorClassProtocol(Protocol): 

30 """ 

31 Protocol for Processor classes. 

32 

33 .. versionadded:: v2.0.0 

34 

35 :cvar plugin_name: The name of the plugin. 

36 :cvar plugin_qualname: The qualified name of the plugin. 

37 :cvar __name__: The name of the class. 

38 """ 

39 

40 plugin_name: str 

41 plugin_qualname: str 

42 __name__: str 

43 

44 def __call__(self, *args: Any, **kwargs: Any) -> Processor: 

45 """ 

46 Instantiate the Processor class. 

47 

48 :param args: Positional arguments for the Processor constructor. 

49 :param kwargs: Keyword arguments for the Processor constructor. 

50 :return: An instance of Processor. 

51 :rtype: Processor 

52 """ 

53 ... # pragma: no cov 

54 

55 

56@runtime_checkable 

57class UserInterfaceClassProtocol(Protocol): 

58 """ 

59 Protocol for UserInterface classes. 

60 

61 .. versionadded:: v2.0.0 

62 

63 :cvar plugin_name: The name of the plugin. 

64 :cvar plugin_qualname: The qualified name of the plugin. 

65 :cvar name: The name of the user interface. 

66 """ 

67 

68 plugin_name: str 

69 plugin_qualname: str 

70 name: str 

71 

72 def __call__(self, *args: Any, **kwargs: Any) -> UserInterfaceBase: 

73 """ 

74 Instantiate the UserInterfaceBase class. 

75 

76 :param args: Positional arguments for the UserInterfaceBase constructor. 

77 :param kwargs: Keyword arguments for the UserInterfaceBase constructor. 

78 :return: An instance of UserInterfaceBase. 

79 :rtype: UserInterfaceBase 

80 """ 

81 ... # pragma: no cov 

82 

83 

84T = TypeVar('T') # Class type (e.g., Processor class) 

85"""The class type to be used for the generic lazy import plugin.""" 

86R = TypeVar('R') # Instance type (e.g., Processor instance) 

87"""The instance type to be used for the generic lazy import plugin.""" 

88 

89 

90class LazyImportPlugin(Generic[T, R], ABC): 

91 """ 

92 Proxy object that lazily imports a plugin class only when accessed. 

93 Thread-safe and transparent to the user. 

94 

95 .. versionadded:: v2.0.0 

96 """ 

97 

98 def __init__(self, module: str, class_name: str) -> None: 

99 """ 

100 Constructor parameter: 

101 

102 :param module: The module name where the class is located. 

103 :type module: str 

104 :param class_name: The name of the class to be lazily imported. 

105 :type class_name: str 

106 """ 

107 self._module = module 

108 self._class_name = class_name 

109 self._cached: Type[T] | None = None 

110 self._lock = threading.Lock() 

111 

112 self.plugin_name = class_name 

113 self.plugin_qualname = f'{module}.{class_name}' 

114 

115 @abstractmethod 

116 def _post_load(self, cls: Type[T]) -> Type[T]: 

117 """ 

118 Perform operations after loading the class. 

119 

120 :param cls: The class type that has been loaded. 

121 :type cls: Type[T] 

122 :return: The class type after post-load operations. 

123 :rtype: Type[T] 

124 """ 

125 return cls # pragma: no cov 

126 

127 def _load(self) -> Type[T]: 

128 """ 

129 Load the class from the specified module. 

130 

131 :return: The loaded class type. 

132 :rtype: Type[T] 

133 """ 

134 if self._cached is None: 

135 with self._lock: 

136 module = importlib.import_module(self._module) 

137 cls = getattr(module, self._class_name) 

138 self._cached = self._post_load(cls) 

139 return self._cached 

140 

141 # Allow calling class attributes transparently 

142 def __getattr__(self, item: str) -> Any: 

143 """ 

144 Access attributes of the lazily loaded class. 

145 

146 :param item: The attribute name to access. 

147 :type item: str 

148 :return: The attribute value. 

149 :rtype: Any 

150 """ 

151 return getattr(self._load(), item) 

152 

153 # Allow instantiation: LazyPlugin() behaves like ActualClass() 

154 def __call__(self, *args: Any, **kwargs: Any) -> R: 

155 """ 

156 Instantiate the lazily loaded class. 

157 

158 :param args: Positional arguments for the class constructor. 

159 :param kwargs: Keyword arguments for the class constructor. 

160 :return: An instance of the class. 

161 :rtype: R 

162 """ 

163 cls = self._load() 

164 return cast(R, cls(*args, **kwargs)) 

165 

166 def __repr__(self) -> str: 

167 """ 

168 Return a string representation of the LazyImportPlugin. 

169 

170 :return: The string representation. 

171 :rtype: str 

172 """ 

173 return f'LazyImportPlugin("{self._module}", "{self._class_name}")' # pragma: no cov 

174 

175 

176class LazyImportProcessor(LazyImportPlugin[Processor, Processor]): 

177 """ 

178 Lazy import proxy for Processor classes. 

179 

180 .. versionadded:: v2.0.0 

181 """ 

182 

183 def _post_load(self, cls: Type[Processor]) -> Type[Processor]: 

184 """ 

185 Perform operations after loading the Processor class. 

186 

187 :param cls: The Processor class type that has been loaded. 

188 :type cls: Type[Processor] 

189 :return: The Processor class type after post-load operations. 

190 :rtype: Type[Processor] 

191 """ 

192 return cls 

193 

194 def __repr__(self) -> str: 

195 """ 

196 Return a string representation of the LazyImportProcessor. 

197 

198 :return: The string representation. 

199 :rtype: str 

200 """ 

201 return f'LazyImportProcessor("{self._module}", "{self._class_name}")' 

202 

203 

204class LazyImportUserInterface(LazyImportPlugin[UserInterfaceBase, UserInterfaceBase]): 

205 """ 

206 Lazy import proxy for UserInterface classes. 

207 

208 .. versionadded:: v2.0.0 

209 """ 

210 

211 def __init__(self, module: str, class_name: str, ui_name: str) -> None: 

212 """ 

213 Constructor parameter: 

214 

215 :param module: The module name where the class is located. 

216 :type module: str 

217 :param class_name: The name of the class to be lazily imported. 

218 :type class_name: str 

219 :param ui_name: The expected name of the user interface. 

220 :type ui_name: str 

221 """ 

222 super().__init__(module, class_name) 

223 self.name = ui_name 

224 

225 def _post_load(self, cls: Type[UserInterfaceBase]) -> Type[UserInterfaceBase]: 

226 """ 

227 Perform operations after loading the UserInterface class. 

228 

229 :param cls: The UserInterface class type that has been loaded. 

230 :type cls: Type[UserInterfaceBase] 

231 :return: The UserInterface class type after post-load operations. 

232 :rtype: Type[UserInterfaceBase] 

233 :raises ValueError: If the class name is inconsistent with the expected name. 

234 """ 

235 if getattr(cls, 'name', None) != self.name: 

236 raise ValueError(f'UserInterface class {cls} has inconsistent .name: expected {self.name}') 

237 return cls 

238 

239 def __repr__(self) -> str: 

240 """ 

241 Return a string representation of the LazyImportUserInterface. 

242 

243 :return: The string representation. 

244 :rtype: str 

245 """ 

246 return f'LazyImportUserInterface("{self._module}", "{self._class_name}", "{self.name}")'