Coverage for C:\leo.repo\leo-editor\leo\core\leoPlugins.py: 22%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

336 statements  

1#@+leo-ver=5-thin 

2#@+node:ekr.20031218072017.3439: * @file leoPlugins.py 

3"""Classes relating to Leo's plugin architecture.""" 

4import sys 

5from typing import List 

6from leo.core import leoGlobals as g 

7# Define modules that may be enabled by default 

8# but that mignt not load because imports may fail. 

9optional_modules = [ 

10 'leo.plugins.livecode', 

11 'leo.plugins.cursesGui2', 

12] 

13#@+others 

14#@+node:ekr.20100908125007.6041: ** Top-level functions (leoPlugins.py) 

15def init(): 

16 """Init g.app.pluginsController.""" 

17 g.app.pluginsController = LeoPluginsController() 

18 

19def registerHandler(tags, fn): 

20 """A wrapper so plugins can still call leoPlugins.registerHandler.""" 

21 return g.app.pluginsController.registerHandler(tags, fn) 

22#@+node:ville.20090222141717.2: ** TryNext (Exception) 

23class TryNext(Exception): 

24 """Try next hook exception. 

25 

26 Raise this in your hook function to indicate that the next hook handler 

27 should be used to handle the operation. If you pass arguments to the 

28 constructor those arguments will be used by the next hook instead of the 

29 original ones. 

30 """ 

31 

32 def __init__(self, *args, **kwargs): 

33 super().__init__() 

34 self.args = args 

35 self.kwargs = kwargs 

36#@+node:ekr.20100908125007.6033: ** class CommandChainDispatcher 

37class CommandChainDispatcher: 

38 """ Dispatch calls to a chain of commands until some func can handle it 

39 

40 Usage: instantiate, execute "add" to add commands (with optional 

41 priority), execute normally via f() calling mechanism. 

42 

43 """ 

44 

45 def __init__(self, commands=None): 

46 if commands is None: 

47 self.chain = [] 

48 else: 

49 self.chain = commands 

50 

51 def __call__(self, *args, **kw): 

52 """ Command chain is called just like normal func. 

53 

54 This will call all funcs in chain with the same args as were given to this 

55 function, and return the result of first func that didn't raise 

56 TryNext """ 

57 for prio, cmd in self.chain: 

58 #print "prio",prio,"cmd",cmd #dbg 

59 try: 

60 ret = cmd(*args, **kw) 

61 return ret 

62 except TryNext as exc: 

63 if exc.args or exc.kwargs: 

64 args = exc.args 

65 kw = exc.kwargs 

66 # if no function will accept it, raise TryNext up to the caller 

67 raise TryNext 

68 

69 def __str__(self): 

70 return str(self.chain) 

71 

72 def add(self, func, priority=0): 

73 """ Add a func to the cmd chain with given priority """ 

74 self.chain.append((priority, func),) 

75 self.chain.sort(key=lambda z: z[0]) 

76 

77 def __iter__(self): 

78 """ Return all objects in chain. 

79 

80 Handy if the objects are not callable. 

81 """ 

82 return iter(self.chain) 

83#@+node:ekr.20100908125007.6009: ** class BaseLeoPlugin 

84class BaseLeoPlugin: 

85 #@+<<docstring>> 

86 #@+node:ekr.20100908125007.6010: *3* <<docstring>> 

87 """A Convenience class to simplify plugin authoring 

88 

89 .. contents:: 

90 

91 Usage 

92 ===== 

93 

94 Initialization 

95 -------------- 

96 

97 - import the base class:: 

98 

99 from leoPlugins from leo.core import leoBasePlugin 

100 

101 - create a class which inherits from leoBasePlugin:: 

102 

103 class myPlugin(leoBasePlugin): 

104 

105 - in the __init__ method of the class, call the parent constructor:: 

106 

107 def __init__(self, tag, keywords): 

108 super().__init__(tag, keywords) 

109 

110 - put the actual plugin code into a method; for this example, the work 

111 is done by myPlugin.handler() 

112 

113 - put the class in a file which lives in the <LeoDir>/plugins directory 

114 for this example it is named myPlugin.py 

115 

116 - add code to register the plugin:: 

117 

118 leoPlugins.registerHandler("after-create-leo-frame", Hello) 

119 

120 Configuration 

121 ------------- 

122 

123 BaseLeoPlugins has 3 *methods* for setting commands 

124 

125 - setCommand:: 

126 

127 def setCommand(self, commandName, handler, 

128 shortcut = None, pane = 'all', verbose = True): 

129 

130 - setMenuItem:: 

131 

132 def setMenuItem(self, menu, commandName = None, handler = None): 

133 

134 - setButton:: 

135 

136 def setButton(self, buttonText = None, commandName = None, color = None): 

137 

138 *variables* 

139 

140 :commandName: the string typed into minibuffer to execute the ``handler`` 

141 

142 :handler: the method in the class which actually does the work 

143 

144 :shortcut: the key combination to activate the command 

145 

146 :menu: a string designating on of the menus ('File', Edit', 'Outline', ...) 

147 

148 :buttonText: the text to put on the button if one is being created. 

149 

150 Example 

151 ======= 

152 

153 Contents of file ``<LeoDir>/plugins/hello.py``:: 

154 

155 class Hello(BaseLeoPlugin): 

156 def __init__(self, tag, keywords): 

157 

158 # call parent __init__ 

159 super().__init__(tag, keywords) 

160 

161 # if the plugin object defines only one command, 

162 # just give it a name. You can then create a button and menu entry 

163 self.setCommand('Hello', self.hello) 

164 self.setButton() 

165 self.setMenuItem('Cmds') 

166 

167 # create a command with a shortcut 

168 self.setCommand('Hola', self.hola, 'Alt-Ctrl-H') 

169 

170 # create a button using different text than commandName 

171 self.setButton('Hello in Spanish') 

172 

173 # create a menu item with default text 

174 self.setMenuItem('Cmds') 

175 

176 # define a command using setMenuItem 

177 self.setMenuItem('Cmds', 'Ciao baby', self.ciao) 

178 

179 def hello(self, event): 

180 g.pr("hello from node %s" % self.c.p.h) 

181 

182 def hola(self, event): 

183 g.pr("hola from node %s" % self.c.p.h) 

184 

185 def ciao(self, event): 

186 g.pr("ciao baby (%s)" % self.c.p.h) 

187 

188 leoPlugins.registerHandler("after-create-leo-frame", Hello) 

189 

190 """ 

191 #@-<<docstring>> 

192 #@+others 

193 #@+node:ekr.20100908125007.6012: *3* __init__ (BaseLeoPlugin) 

194 def __init__(self, tag, keywords): 

195 """Set self.c to be the ``commander`` of the active node 

196 """ 

197 self.c = keywords['c'] 

198 self.commandNames = [] 

199 #@+node:ekr.20100908125007.6013: *3* setCommand 

200 def setCommand(self, commandName, handler, 

201 shortcut='', pane='all', verbose=True): 

202 """Associate a command name with handler code, 

203 optionally defining a keystroke shortcut 

204 """ 

205 self.commandNames.append(commandName) 

206 self.commandName = commandName 

207 self.shortcut = shortcut 

208 self.handler = handler 

209 self.c.k.registerCommand(commandName, handler, 

210 pane=pane, shortcut=shortcut, verbose=verbose) 

211 #@+node:ekr.20100908125007.6014: *3* setMenuItem 

212 def setMenuItem(self, menu, commandName=None, handler=None): 

213 """Create a menu item in 'menu' using text 'commandName' calling handler 'handler' 

214 if commandName and handler are none, use the most recently defined values 

215 """ 

216 # setMenuItem can create a command, or use a previously defined one. 

217 if commandName is None: 

218 commandName = self.commandName 

219 # make sure commandName is in the list of commandNames 

220 else: 

221 if commandName not in self.commandNames: 

222 self.commandNames.append(commandName) 

223 if handler is None: 

224 handler = self.handler 

225 table = ((commandName, None, handler),) 

226 self.c.frame.menu.createMenuItemsFromTable(menu, table) 

227 #@+node:ekr.20100908125007.6015: *3* setButton 

228 def setButton(self, buttonText=None, commandName=None, color=None): 

229 """Associate an existing command with a 'button' 

230 """ 

231 if buttonText is None: 

232 buttonText = self.commandName 

233 if commandName is None: 

234 commandName = self.commandName 

235 else: 

236 if commandName not in self.commandNames: 

237 raise NameError(f"setButton error, {commandName} is not a commandName") 

238 if color is None: 

239 color = 'grey' 

240 script = f"c.k.simulateCommand('{self.commandName}')" 

241 g.app.gui.makeScriptButton( 

242 self.c, 

243 args=None, 

244 script=script, 

245 buttonText=buttonText, bg=color) 

246 #@-others 

247#@+node:ekr.20100908125007.6007: ** class LeoPluginsController 

248class LeoPluginsController: 

249 """The global plugins controller, g.app.pluginsController""" 

250 #@+others 

251 #@+node:ekr.20100909065501.5954: *3* plugins.Birth 

252 #@+node:ekr.20100908125007.6034: *4* plugins.ctor & reloadSettings 

253 def __init__(self): 

254 

255 # Keys are tags, values are lists of bunches. 

256 self.handlers = {} 

257 # Keys are regularized module names, values are the names of .leo files 

258 # containing @enabled-plugins nodes that caused the plugin to be loaded 

259 self.loadedModulesFilesDict = {} 

260 # Keys are regularized module names, values are modules. 

261 self.loadedModules = {} 

262 # The stack of module names. The top is the module being loaded. 

263 self.loadingModuleNameStack = [] 

264 self.signonModule = None # A hack for plugin_signon. 

265 # Settings. Set these here in case finishCreate is never called. 

266 self.warn_on_failure = True 

267 g.act_on_node = CommandChainDispatcher() 

268 g.visit_tree_item = CommandChainDispatcher() 

269 g.tree_popup_handlers = [] 

270 #@+node:ekr.20100909065501.5974: *4* plugins.finishCreate & reloadSettings 

271 def finishCreate(self): 

272 self.reloadSettings() 

273 

274 def reloadSettings(self): 

275 self.warn_on_failure = g.app.config.getBool( 

276 'warn_when_plugins_fail_to_load', default=True) 

277 #@+node:ekr.20100909065501.5952: *3* plugins.Event handlers 

278 #@+node:ekr.20161029060545.1: *4* plugins.on_idle 

279 def on_idle(self): 

280 """Call all idle-time hooks.""" 

281 if g.app.idle_time_hooks_enabled: 

282 for frame in g.app.windowList: 

283 c = frame.c 

284 # Do NOT compute c.currentPosition. 

285 # This would be a MAJOR leak of positions. 

286 g.doHook("idle", c=c) 

287 #@+node:ekr.20100908125007.6017: *4* plugins.doHandlersForTag & helper 

288 def doHandlersForTag(self, tag, keywords): 

289 """ 

290 Execute all handlers for a given tag, in alphabetical order. 

291 The caller, doHook, catches all exceptions. 

292 """ 

293 if g.app.killed: 

294 return None 

295 # 

296 # Execute hooks in some random order. 

297 # Return if one of them returns a non-None result. 

298 for bunch in self.handlers.get(tag, []): 

299 val = self.callTagHandler(bunch, tag, keywords) 

300 if val is not None: 

301 return val 

302 if 'all' in self.handlers: 

303 bunches = self.handlers.get('all') 

304 for bunch in bunches: 

305 self.callTagHandler(bunch, tag, keywords) 

306 return None 

307 #@+node:ekr.20100908125007.6016: *5* plugins.callTagHandler 

308 def callTagHandler(self, bunch, tag, keywords): 

309 """Call the event handler.""" 

310 handler, moduleName = bunch.fn, bunch.moduleName 

311 # Make sure the new commander exists. 

312 for key in ('c', 'new_c'): 

313 c = keywords.get(key) 

314 if c: 

315 # Make sure c exists and has a frame. 

316 if not c.exists or not hasattr(c, 'frame'): 

317 # g.pr('skipping tag %s: c does not exist or does not have a frame.' % tag) 

318 return None 

319 # Calls to registerHandler from inside the handler belong to moduleName. 

320 self.loadingModuleNameStack.append(moduleName) 

321 try: 

322 result = handler(tag, keywords) 

323 except Exception: 

324 g.es(f"hook failed: {tag}, {handler}, {moduleName}") 

325 g.es_exception() 

326 result = None 

327 self.loadingModuleNameStack.pop() 

328 return result 

329 #@+node:ekr.20100908125007.6018: *4* plugins.doPlugins (g.app.hookFunction) 

330 def doPlugins(self, tag, keywords): 

331 """The default g.app.hookFunction.""" 

332 if g.app.killed: 

333 return None 

334 if tag in ('start1', 'open0'): 

335 self.loadHandlers(tag, keywords) 

336 return self.doHandlersForTag(tag, keywords) 

337 #@+node:ekr.20100909065501.5950: *3* plugins.Information 

338 #@+node:ekr.20100908125007.6019: *4* plugins.getHandlersForTag 

339 def getHandlersForTag(self, tags): 

340 if isinstance(tags, (list, tuple)): 

341 result = [] 

342 for tag in tags: 

343 aList = self.getHandlersForOneTag(tag) 

344 result.extend(aList) 

345 return result 

346 return self.getHandlersForOneTag(tags) 

347 

348 def getHandlersForOneTag(self, tag): 

349 return self.handlers.get(tag, []) 

350 #@+node:ekr.20100910075900.10204: *4* plugins.getLoadedPlugins 

351 def getLoadedPlugins(self): 

352 return list(self.loadedModules.keys()) 

353 #@+node:ekr.20100908125007.6020: *4* plugins.getPluginModule 

354 def getPluginModule(self, moduleName): 

355 return self.loadedModules.get(moduleName) 

356 #@+node:ekr.20100908125007.6021: *4* plugins.isLoaded 

357 def isLoaded(self, fn): 

358 return self.regularizeName(fn) in self.loadedModules 

359 #@+node:ekr.20100908125007.6025: *4* plugins.printHandlers 

360 def printHandlers(self, c): 

361 """Print the handlers for each plugin.""" 

362 tabName = 'Plugins' 

363 c.frame.log.selectTab(tabName) 

364 g.es_print('all plugin handlers...\n', tabName=tabName) 

365 data = [] 

366 # keys are module names: values are lists of tags. 

367 modules_d: dict[str, List[str]] = {} 

368 for tag in self.handlers: 

369 bunches = self.handlers.get(tag) 

370 for bunch in bunches: 

371 fn = bunch.fn 

372 name = bunch.moduleName 

373 tags = modules_d.get(name, []) 

374 tags.append(tag) 

375 key = f"{name}.{fn.__name__}" 

376 modules_d[key] = tags 

377 n = 4 

378 for module in sorted(modules_d): 

379 tags = modules_d.get(module) 

380 for tag in tags: 

381 n = max(n, len(tag)) 

382 data.append((tag, module),) 

383 lines = sorted(list(set( 

384 ["%*s %s\n" % (-n, s1, s2) for (s1, s2) in data]))) 

385 g.es_print('', ''.join(lines), tabName=tabName) 

386 #@+node:ekr.20100908125007.6026: *4* plugins.printPlugins 

387 def printPlugins(self, c): 

388 """Print all enabled plugins.""" 

389 tabName = 'Plugins' 

390 c.frame.log.selectTab(tabName) 

391 data = [] 

392 data.append('enabled plugins...\n') 

393 for z in sorted(self.loadedModules): 

394 data.append(z) 

395 lines = [f"{z}\n" for z in data] 

396 g.es('', ''.join(lines), tabName=tabName) 

397 #@+node:ekr.20100908125007.6027: *4* plugins.printPluginsInfo 

398 def printPluginsInfo(self, c): 

399 """ 

400 Print the file name responsible for loading a plugin. 

401 

402 This is the first .leo file containing an @enabled-plugins node 

403 that enables the plugin. 

404 """ 

405 d = self.loadedModulesFilesDict 

406 tabName = 'Plugins' 

407 c.frame.log.selectTab(tabName) 

408 data = [] 

409 n = 4 

410 for moduleName in d: 

411 fileName = d.get(moduleName) 

412 n = max(n, len(moduleName)) 

413 data.append((moduleName, fileName),) 

414 lines = ["%*s %s\n" % (-n, s1, s2) for (s1, s2) in data] 

415 g.es('', ''.join(lines), tabName=tabName) 

416 #@+node:ekr.20100909065501.5949: *4* plugins.regularizeName 

417 def regularizeName(self, moduleOrFileName): 

418 """ 

419 Return the module name used as a key to this modules dictionaries. 

420 

421 We *must* allow .py suffixes, for compatibility with @enabled-plugins nodes. 

422 """ 

423 if not moduleOrFileName.endswith('.py'): 

424 # A module name. Return it unchanged. 

425 return moduleOrFileName 

426 # 

427 # 1880: The legacy code implictly assumed that os.path.dirname(fn) was empty! 

428 # The new code explicitly ignores any directories in the path. 

429 fn = g.os_path_basename(moduleOrFileName) 

430 return "leo.plugins." + fn[:-3] 

431 #@+node:ekr.20100909065501.5953: *3* plugins.Load & unload 

432 #@+node:ekr.20100908125007.6022: *4* plugins.loadHandlers 

433 def loadHandlers(self, tag, keys): 

434 """ 

435 Load all enabled plugins. 

436 

437 Using a module name (without the trailing .py) allows a plugin to 

438 be loaded from outside the leo/plugins directory. 

439 """ 

440 

441 def pr(*args, **keys): 

442 if not g.unitTesting: 

443 g.es_print(*args, **keys) 

444 

445 s = g.app.config.getEnabledPlugins() 

446 if not s: 

447 return 

448 if tag == 'open0' and not g.app.silentMode and not g.app.batchMode: 

449 if 0: 

450 s2 = f"@enabled-plugins found in {g.app.config.enabledPluginsFileName}" 

451 g.blue(s2) 

452 for plugin in s.splitlines(): 

453 if plugin.strip() and not plugin.lstrip().startswith('#'): 

454 self.loadOnePlugin(plugin.strip(), tag=tag) 

455 #@+node:ekr.20100908125007.6024: *4* plugins.loadOnePlugin & helper functions 

456 def loadOnePlugin(self, moduleOrFileName, tag='open0', verbose=False): 

457 """ 

458 Load one plugin from a file name or module. 

459 Use extensive tracing if --trace-plugins is in effect. 

460 

461 Using a module name allows plugins to be loaded from outside the leo/plugins directory. 

462 """ 

463 global optional_modules 

464 trace = 'plugins' in g.app.debug 

465 

466 def report(message): 

467 if trace and not g.unitTesting: 

468 g.es_print(f"loadOnePlugin: {message}") 

469 

470 # Define local helper functions. 

471 #@+others 

472 #@+node:ekr.20180528160855.1: *5* function:callInitFunction 

473 def callInitFunction(result): 

474 """True to call the top-level init function.""" 

475 try: 

476 # Indicate success only if init_result is True. 

477 # Careful: this may throw an exception. 

478 init_result = result.init() 

479 if init_result not in (True, False): 

480 report(f"{moduleName}.init() did not return a bool") 

481 if init_result: 

482 self.loadedModules[moduleName] = result 

483 self.loadedModulesFilesDict[moduleName] = ( 

484 g.app.config.enabledPluginsFileName 

485 ) 

486 else: 

487 report(f"{moduleName}.init() returned False") 

488 result = None 

489 except Exception: 

490 report(f"exception loading plugin: {moduleName}") 

491 g.es_exception() 

492 result = None 

493 return result 

494 #@+node:ekr.20180528162604.1: *5* function:finishImport 

495 def finishImport(result): 

496 """Handle last-minute checks.""" 

497 if tag == 'unit-test-load': 

498 return result # Keep the result, but do no more. 

499 if hasattr(result, 'init'): 

500 return callInitFunction(result) 

501 # 

502 # No top-level init function. 

503 if g.unitTesting: 

504 # Do *not* load the module. 

505 self.loadedModules[moduleName] = None 

506 return None 

507 # Guess that the module was loaded correctly. 

508 report(f"fyi: no top-level init() function in {moduleName}") 

509 self.loadedModules[moduleName] = result 

510 return result 

511 #@+node:ekr.20180528160744.1: *5* function:loadOnePluginHelper 

512 def loadOnePluginHelper(moduleName): 

513 result = None 

514 try: 

515 __import__(moduleName) 

516 # Look up through sys.modules, __import__ returns toplevel package 

517 result = sys.modules[moduleName] 

518 except g.UiTypeException: 

519 report(f"plugin {moduleName} does not support {g.app.gui.guiName()} gui") 

520 except ImportError: 

521 report(f"error importing plugin: {moduleName}") 

522 # except ModuleNotFoundError: 

523 # report('module not found: %s' % moduleName) 

524 except SyntaxError: 

525 report(f"syntax error importing plugin: {moduleName}") 

526 except Exception: 

527 report(f"exception importing plugin: {moduleName}") 

528 g.es_exception() 

529 return result 

530 #@+node:ekr.20180528162300.1: *5* function:reportFailedImport 

531 def reportFailedImport(): 

532 """Report a failed import.""" 

533 if g.app.batchMode or g.app.inBridge or g.unitTesting: 

534 return 

535 if ( 

536 self.warn_on_failure and 

537 tag == 'open0' and 

538 not g.app.gui.guiName().startswith('curses') and 

539 moduleName not in optional_modules 

540 ): 

541 report(f"can not load enabled plugin: {moduleName}") 

542 #@-others 

543 if not g.app.enablePlugins: 

544 report(f"plugins disabled: {moduleOrFileName}") 

545 return None 

546 if moduleOrFileName.startswith('@'): 

547 report(f"ignoring Leo directive: {moduleOrFileName}") 

548 return None 

549 # Return None, not False, to keep pylint happy. 

550 # Allow Leo directives in @enabled-plugins nodes. 

551 moduleName = self.regularizeName(moduleOrFileName) 

552 if self.isLoaded(moduleName): 

553 module = self.loadedModules.get(moduleName) 

554 return module 

555 assert g.app.loadDir 

556 moduleName = g.toUnicode(moduleName) 

557 # 

558 # Try to load the plugin. 

559 try: 

560 self.loadingModuleNameStack.append(moduleName) 

561 result = loadOnePluginHelper(moduleName) 

562 finally: 

563 self.loadingModuleNameStack.pop() 

564 if not result: 

565 if trace: 

566 reportFailedImport() 

567 return None 

568 # 

569 # Last-minute checks. 

570 try: 

571 self.loadingModuleNameStack.append(moduleName) 

572 result = finishImport(result) 

573 finally: 

574 self.loadingModuleNameStack.pop() 

575 if result: 

576 # #1688: Plugins can update globalDirectiveList. 

577 # Recalculate g.directives_pat. 

578 g.update_directives_pat() 

579 report(f"loaded: {moduleName}") 

580 self.signonModule = result # for self.plugin_signon. 

581 return result 

582 #@+node:ekr.20031218072017.1318: *4* plugins.plugin_signon 

583 def plugin_signon(self, module_name, verbose=False): 

584 """Print the plugin signon.""" 

585 # This is called from as the result of the imports 

586 # in self.loadOnePlugin 

587 m = self.signonModule 

588 if verbose: 

589 g.es(f"...{m.__name__}.py v{m.__version__}: {g.plugin_date(m)}") 

590 g.pr(m.__name__, m.__version__) 

591 self.signonModule = None # Prevent double signons. 

592 #@+node:ekr.20100908125007.6030: *4* plugins.unloadOnePlugin 

593 def unloadOnePlugin(self, moduleOrFileName, verbose=False): 

594 moduleName = self.regularizeName(moduleOrFileName) 

595 if self.isLoaded(moduleName): 

596 if verbose: 

597 g.pr('unloading', moduleName) 

598 del self.loadedModules[moduleName] 

599 for tag in self.handlers: 

600 bunches = self.handlers.get(tag) 

601 bunches = [bunch for bunch in bunches if bunch.moduleName != moduleName] 

602 self.handlers[tag] = bunches 

603 #@+node:ekr.20100909065501.5951: *3* plugins.Registration 

604 #@+node:ekr.20100908125007.6028: *4* plugins.registerExclusiveHandler 

605 def registerExclusiveHandler(self, tags, fn): 

606 """ Register one or more exclusive handlers""" 

607 if isinstance(tags, (list, tuple)): 

608 for tag in tags: 

609 self.registerOneExclusiveHandler(tag, fn) 

610 else: 

611 self.registerOneExclusiveHandler(tags, fn) 

612 

613 def registerOneExclusiveHandler(self, tag, fn): 

614 """Register one exclusive handler""" 

615 try: 

616 moduleName = self.loadingModuleNameStack[-1] 

617 except IndexError: 

618 moduleName = '<no module>' 

619 # print(f"{g.unitTesting:6} {moduleName:15} {tag:25} {fn.__name__}") 

620 if g.unitTesting: 

621 return 

622 if tag in self.handlers: 

623 g.es(f"*** Two exclusive handlers for '{tag}'") 

624 else: 

625 bunch = g.Bunch(fn=fn, moduleName=moduleName, tag='handler') 

626 aList = self.handlers.get(tag, []) 

627 aList.append(bunch) 

628 self.handlers[tag] = aList 

629 #@+node:ekr.20100908125007.6029: *4* plugins.registerHandler & registerOneHandler 

630 def registerHandler(self, tags, fn): 

631 """ Register one or more handlers""" 

632 if isinstance(tags, (list, tuple)): 

633 for tag in tags: 

634 self.registerOneHandler(tag, fn) 

635 else: 

636 self.registerOneHandler(tags, fn) 

637 

638 def registerOneHandler(self, tag, fn): 

639 """Register one handler""" 

640 try: 

641 moduleName = self.loadingModuleNameStack[-1] 

642 except IndexError: 

643 moduleName = '<no module>' 

644 # print(f"{g.unitTesting:6} {moduleName:15} {tag:25} {fn.__name__}") 

645 items = self.handlers.get(tag, []) 

646 functions = [z.fn for z in items] 

647 if fn not in functions: # Vitalije 

648 bunch = g.Bunch(fn=fn, moduleName=moduleName, tag='handler') 

649 items.append(bunch) 

650 self.handlers[tag] = items 

651 #@+node:ekr.20100908125007.6031: *4* plugins.unregisterHandler 

652 def unregisterHandler(self, tags, fn): 

653 if isinstance(tags, (list, tuple)): 

654 for tag in tags: 

655 self.unregisterOneHandler(tag, fn) 

656 else: 

657 self.unregisterOneHandler(tags, fn) 

658 

659 def unregisterOneHandler(self, tag, fn): 

660 bunches = self.handlers.get(tag) 

661 bunches = [bunch for bunch in bunches if bunch and bunch.fn != fn] 

662 self.handlers[tag] = bunches 

663 #@-others 

664#@-others 

665#@@language python 

666#@@tabwidth -4 

667#@@pagewidth 70 

668 

669#@-leo