Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2 lml.plugin 

3 ~~~~~~~~~~~~~~~~~~~ 

4 

5 lml divides the plugins into two category: load-me-later plugins and 

6 load-me-now ones. load-me-later plugins refer to the plugins were 

7 loaded when needed due its bulky and/or memory hungry dependencies. 

8 Those plugins has to use lml and respect lml's design principle. 

9 

10 load-me-now plugins refer to the plugins are immediately imported. All 

11 conventional Python classes are by default immediately imported. 

12 

13 :class:`~lml.plugin.PluginManager` should be inherited to form new 

14 plugin manager class. If you have more than one plugins in your 

15 architecture, it is advisable to have one class per plugin type. 

16 

17 :class:`~lml.plugin.PluginInfoChain` helps the plugin module to 

18 declare the available plugins in the module. 

19 

20 :class:`~lml.plugin.PluginInfo` can be subclassed to describe 

21 your plugin. Its method :meth:`~lml.plugin.PluginInfo.tags` 

22 can be overridden to help its matching :class:`~lml.plugin.PluginManager` 

23 to look itself up. 

24 

25 :copyright: (c) 2017-2020 by Onni Software Ltd. 

26 :license: New BSD License, see LICENSE for more details 

27""" 

28import logging 

29from collections import defaultdict 

30 

31from lml.utils import json_dumps, do_import_class 

32 

33PLUG_IN_MANAGERS = {} 

34CACHED_PLUGIN_INFO = defaultdict(list) 

35 

36log = logging.getLogger(__name__) 

37 

38 

39class PluginInfo(object): 

40 """ 

41 Information about the plugin. 

42 

43 It is used together with PluginInfoChain to describe the plugins. 

44 Meanwhile, it is a class decorator and can be used to register a plugin 

45 immediately for use, in other words, the PluginInfo decorated plugin 

46 class is not loaded later. 

47 

48 Parameters 

49 ------------- 

50 name: 

51 plugin name 

52 

53 absolute_import_path: 

54 absolute import path from your plugin name space for your plugin class 

55 

56 tags: 

57 a list of keywords help the plugin manager to retrieve your plugin 

58 

59 keywords: 

60 Another custom properties. 

61 

62 Examples 

63 ------------- 

64 

65 For load-me-later plugins: 

66 

67 >>> info = PluginInfo("sample", 

68 ... abs_class_path='lml.plugin.PluginInfo', # demonstration only. 

69 ... tags=['load-me-later'], 

70 ... custom_property = 'I am a custom property') 

71 >>> print(info.module_name) 

72 lml 

73 >>> print(info.custom_property) 

74 I am a custom property 

75 

76 For load-me-now plugins: 

77 

78 >>> @PluginInfo("sample", tags=['load-me-now']) 

79 ... class TestPlugin: 

80 ... def echo(self, words): 

81 ... print("echoing %s" % words) 

82 

83 Now let's retrive the second plugin back: 

84 

85 >>> class SamplePluginManager(PluginManager): 

86 ... def __init__(self): 

87 ... PluginManager.__init__(self, "sample") 

88 >>> sample_manager = SamplePluginManager() 

89 >>> test_plugin=sample_manager.get_a_plugin("load-me-now") 

90 >>> test_plugin.echo("hey..") 

91 echoing hey.. 

92 

93 """ 

94 

95 def __init__( 

96 self, plugin_type, abs_class_path=None, tags=None, **keywords 

97 ): 

98 self.plugin_type = plugin_type 

99 self.absolute_import_path = abs_class_path 

100 self.cls = None 

101 self.properties = keywords 

102 self.__tags = tags 

103 

104 def __getattr__(self, name): 

105 if name == "module_name": 

106 if self.absolute_import_path: 

107 module_name = self.absolute_import_path.split(".")[0] 

108 else: 

109 module_name = self.cls.__module__ 

110 return module_name 

111 return self.properties.get(name) 

112 

113 def tags(self): 

114 """ 

115 A list of tags for identifying the plugin class 

116 

117 The plugin class is described at the absolute_import_path 

118 """ 

119 if self.__tags is None: 

120 yield self.plugin_type 

121 else: 

122 for tag in self.__tags: 

123 yield tag 

124 

125 def __repr__(self): 

126 rep = { 

127 "plugin_type": self.plugin_type, 

128 "path": self.absolute_import_path, 

129 } 

130 rep.update(self.properties) 

131 return json_dumps(rep) 

132 

133 def __call__(self, cls): 

134 self.cls = cls 

135 _register_a_plugin(self, cls) 

136 return cls 

137 

138 

139class PluginInfoChain(object): 

140 """ 

141 Pandas style, chained list declaration 

142 

143 It is used in the plugin packages to list all plugin classes 

144 """ 

145 

146 def __init__(self, path): 

147 self._logger = logging.getLogger( 

148 self.__class__.__module__ + "." + self.__class__.__name__ 

149 ) 

150 self.module_name = path 

151 

152 def add_a_plugin(self, plugin_type, submodule=None, **keywords): 

153 """ 

154 Add a plain plugin 

155 

156 Parameters 

157 ------------- 

158 

159 plugin_type: 

160 plugin manager name 

161 

162 submodule: 

163 the relative import path to your plugin class 

164 """ 

165 a_plugin_info = PluginInfo( 

166 plugin_type, self._get_abs_path(submodule), **keywords 

167 ) 

168 

169 self.add_a_plugin_instance(a_plugin_info) 

170 return self 

171 

172 def add_a_plugin_instance(self, plugin_info_instance): 

173 """ 

174 Add a plain plugin 

175 

176 Parameters 

177 ------------- 

178 

179 plugin_info_instance: 

180 an instance of PluginInfo 

181 

182 The developer has to specify the absolute import path 

183 """ 

184 self._logger.debug( 

185 "add %s as '%s' plugin", 

186 plugin_info_instance.absolute_import_path, 

187 plugin_info_instance.plugin_type, 

188 ) 

189 _load_me_later(plugin_info_instance) 

190 return self 

191 

192 def _get_abs_path(self, submodule): 

193 return "%s.%s" % (self.module_name, submodule) 

194 

195 

196class PluginManager(object): 

197 """ 

198 Load plugin info into in-memory dictionary for later import 

199 

200 Parameters 

201 -------------- 

202 

203 plugin_type: 

204 the plugin type. All plugins of this plugin type will be 

205 registered to it. 

206 """ 

207 

208 def __init__(self, plugin_type): 

209 self.plugin_name = plugin_type 

210 self.registry = defaultdict(list) 

211 self.tag_groups = dict() 

212 self._logger = logging.getLogger( 

213 self.__class__.__module__ + "." + self.__class__.__name__ 

214 ) 

215 _register_class(self) 

216 

217 def get_a_plugin(self, key, **keywords): 

218 """ Get a plugin 

219 

220 Parameters 

221 --------------- 

222 

223 key: 

224 the key to find the plugins 

225 

226 keywords: 

227 additional parameters for help the retrieval of the plugins 

228 """ 

229 self._logger.debug("get a plugin called") 

230 plugin = self.load_me_now(key) 

231 return plugin() 

232 

233 def raise_exception(self, key): 

234 """Raise plugin not found exception 

235 

236 Override this method to raise custom exception 

237 

238 Parameters 

239 ----------------- 

240 

241 key: 

242 the key to find the plugin 

243 """ 

244 self._logger.debug(self.registry.keys()) 

245 raise Exception("No %s is found for %s" % (self.plugin_name, key)) 

246 

247 def load_me_later(self, plugin_info): 

248 """ 

249 Register a plugin info for later loading 

250 

251 Parameters 

252 -------------- 

253 

254 plugin_info: 

255 a instance of plugin info 

256 """ 

257 self._logger.debug("load %s later", plugin_info.absolute_import_path) 

258 self._update_registry_and_expand_tag_groups(plugin_info) 

259 

260 def load_me_now(self, key, library=None, **keywords): 

261 """ 

262 Import a plugin from plugin registry 

263 

264 Parameters 

265 ----------------- 

266 

267 key: 

268 the key to find the plugin 

269 

270 library: 

271 to use a specific plugin module 

272 """ 

273 if keywords: 

274 self._logger.debug(keywords) 

275 __key = key.lower() 

276 

277 if __key in self.registry: 

278 for plugin_info in self.registry[__key]: 

279 cls = self.dynamic_load_library(plugin_info) 

280 module_name = _get_me_pypi_package_name(cls) 

281 if library and module_name != library: 

282 continue 

283 else: 

284 break 

285 else: 

286 # only library condition could raise an exception 

287 self._logger.debug("%s is not installed" % library) 

288 self.raise_exception(key) 

289 self._logger.debug("load %s now for '%s'", cls, key) 

290 return cls 

291 else: 

292 self.raise_exception(key) 

293 

294 def dynamic_load_library(self, a_plugin_info): 

295 """Dynamically load the plugin info if not loaded 

296 

297 

298 Parameters 

299 -------------- 

300 

301 a_plugin_info: 

302 a instance of plugin info 

303 """ 

304 if a_plugin_info.cls is None: 

305 self._logger.debug("import " + a_plugin_info.absolute_import_path) 

306 cls = do_import_class(a_plugin_info.absolute_import_path) 

307 a_plugin_info.cls = cls 

308 return a_plugin_info.cls 

309 

310 def register_a_plugin(self, plugin_cls, plugin_info): 

311 """ for dynamically loaded plugin during runtime 

312 

313 Parameters 

314 -------------- 

315 

316 plugin_cls: 

317 the actual plugin class refered to by the second parameter 

318 

319 plugin_info: 

320 a instance of plugin info 

321 """ 

322 self._logger.debug("register %s", _show_me_your_name(plugin_cls)) 

323 plugin_info.cls = plugin_cls 

324 self._update_registry_and_expand_tag_groups(plugin_info) 

325 

326 def get_primary_key(self, key): 

327 __key = key.lower() 

328 return self.tag_groups.get(__key, None) 

329 

330 def _update_registry_and_expand_tag_groups(self, plugin_info): 

331 primary_tag = None 

332 for index, key in enumerate(plugin_info.tags()): 

333 self.registry[key.lower()].append(plugin_info) 

334 if index == 0: 

335 primary_tag = key.lower() 

336 self.tag_groups[key.lower()] = primary_tag 

337 

338 

339def _register_class(cls): 

340 """Reigister a newly created plugin manager""" 

341 log.debug("declare '%s' plugin manager", cls.plugin_name) 

342 PLUG_IN_MANAGERS[cls.plugin_name] = cls 

343 if cls.plugin_name in CACHED_PLUGIN_INFO: 

344 # check if there is early registrations or not 

345 for plugin_info in CACHED_PLUGIN_INFO[cls.plugin_name]: 

346 if plugin_info.absolute_import_path: 

347 log.debug( 

348 "load cached plugin info: %s", 

349 plugin_info.absolute_import_path, 

350 ) 

351 else: 

352 log.debug( 

353 "load cached plugin info: %s", 

354 _show_me_your_name(plugin_info.cls), 

355 ) 

356 cls.load_me_later(plugin_info) 

357 

358 del CACHED_PLUGIN_INFO[cls.plugin_name] 

359 

360 

361def _register_a_plugin(plugin_info, plugin_cls): 

362 """module level function to register a plugin""" 

363 manager = PLUG_IN_MANAGERS.get(plugin_info.plugin_type) 

364 if manager: 

365 manager.register_a_plugin(plugin_cls, plugin_info) 

366 else: 

367 # let's cache it and wait the manager to be registered 

368 try: 

369 log.debug("caching %s", _show_me_your_name(plugin_cls.__name__)) 

370 except AttributeError: 

371 log.debug("caching %s", _show_me_your_name(plugin_cls)) 

372 CACHED_PLUGIN_INFO[plugin_info.plugin_type].append(plugin_info) 

373 

374 

375def _load_me_later(plugin_info): 

376 """ module level function to load a plugin later""" 

377 manager = PLUG_IN_MANAGERS.get(plugin_info.plugin_type) 

378 if manager: 

379 manager.load_me_later(plugin_info) 

380 else: 

381 # let's cache it and wait the manager to be registered 

382 log.debug( 

383 "caching %s for %s", 

384 plugin_info.absolute_import_path, 

385 plugin_info.plugin_type, 

386 ) 

387 CACHED_PLUGIN_INFO[plugin_info.plugin_type].append(plugin_info) 

388 

389 

390def _get_me_pypi_package_name(module): 

391 try: 

392 module_name = module.__module__ 

393 root_module_name = module_name.split(".")[0] 

394 return root_module_name.replace("_", "-") 

395 except AttributeError: 

396 return None 

397 

398 

399def _show_me_your_name(cls_func_or_data_type): 

400 try: 

401 return cls_func_or_data_type.__name__ 

402 except AttributeError: 

403 return str(type(cls_func_or_data_type))