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#@+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 self.handlers = {} 

256 self.loadedModulesFilesDict = {} 

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.loadedModules = {} 

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

261 self.loadingModuleNameStack = [] 

262 # The stack of module names. 

263 # The top is the module being loaded. 

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 assert g 

268 g.act_on_node = CommandChainDispatcher() 

269 g.visit_tree_item = CommandChainDispatcher() 

270 g.tree_popup_handlers = [] 

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

272 def finishCreate(self): 

273 self.reloadSettings() 

274 

275 def reloadSettings(self): 

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

277 'warn_when_plugins_fail_to_load', default=True) 

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

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

280 def on_idle(self): 

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

282 if g.app.idle_time_hooks_enabled: 

283 for frame in g.app.windowList: 

284 c = frame.c 

285 # Do NOT compute c.currentPosition. 

286 # This would be a MAJOR leak of positions. 

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

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

289 def doHandlersForTag(self, tag, keywords): 

290 """ 

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

292 The caller, doHook, catches all exceptions. 

293 """ 

294 if g.app.killed: 

295 return None 

296 # 

297 # Execute hooks in some random order. 

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

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

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

301 if val is not None: 

302 return val 

303 if 'all' in self.handlers: 

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

305 for bunch in bunches: 

306 self.callTagHandler(bunch, tag, keywords) 

307 return None 

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

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

310 """Call the event handler.""" 

311 handler, moduleName = bunch.fn, bunch.moduleName 

312 # Make sure the new commander exists. 

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

314 c = keywords.get(key) 

315 if c: 

316 # Make sure c exists and has a frame. 

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

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

319 return None 

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

321 self.loadingModuleNameStack.append(moduleName) 

322 try: 

323 result = handler(tag, keywords) 

324 except Exception: 

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

326 g.es_exception() 

327 result = None 

328 self.loadingModuleNameStack.pop() 

329 return result 

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

331 def doPlugins(self, tag, keywords): 

332 """The default g.app.hookFunction.""" 

333 if g.app.killed: 

334 return None 

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

336 self.loadHandlers(tag, keywords) 

337 return self.doHandlersForTag(tag, keywords) 

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

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

340 def getHandlersForTag(self, tags): 

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

342 result = [] 

343 for tag in tags: 

344 aList = self.getHandlersForOneTag(tag) 

345 result.extend(aList) 

346 return result 

347 return self.getHandlersForOneTag(tags) 

348 

349 def getHandlersForOneTag(self, tag): 

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

351 return aList 

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

353 def getLoadedPlugins(self): 

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

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

356 def getPluginModule(self, moduleName): 

357 return self.loadedModules.get(moduleName) 

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

359 def isLoaded(self, fn): 

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

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

362 def printHandlers(self, c, moduleName=None): 

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

364 tabName = 'Plugins' 

365 c.frame.log.selectTab(tabName) 

366 if moduleName: 

367 s = 'handlers for {moduleName}...\n' 

368 else: 

369 s = 'all plugin handlers...\n' 

370 g.es(s + '\n', tabName=tabName) 

371 data = [] 

372 modules: dict[str, List[str]] = {} 

373 for tag in self.handlers: 

374 bunches = self.handlers.get(tag) 

375 for bunch in bunches: 

376 name = bunch.moduleName 

377 tags = modules.get(name, []) 

378 tags.append(tag) 

379 modules[name] = tags 

380 n = 4 

381 for key in sorted(modules): 

382 tags = modules.get(key) 

383 if moduleName in (None, key): 

384 for tag in tags: 

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

386 data.append((tag, key),) 

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

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

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

390 def printPlugins(self, c): 

391 """Print all enabled plugins.""" 

392 tabName = 'Plugins' 

393 c.frame.log.selectTab(tabName) 

394 data = [] 

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

396 for z in sorted(self.loadedModules): 

397 data.append(z) 

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

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

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

401 def printPluginsInfo(self, c): 

402 """ 

403 Print the file name responsible for loading a plugin. 

404 

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

406 that enables the plugin. 

407 """ 

408 d = self.loadedModulesFilesDict 

409 tabName = 'Plugins' 

410 c.frame.log.selectTab(tabName) 

411 data = [] 

412 n = 4 

413 for moduleName in d: 

414 fileName = d.get(moduleName) 

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

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

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

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

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

420 def regularizeName(self, moduleOrFileName): 

421 """ 

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

423 

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

425 """ 

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

427 # A module name. Return it unchanged. 

428 return moduleOrFileName 

429 # 

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

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

432 fn = g.os_path_basename(moduleOrFileName) 

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

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

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

436 def loadHandlers(self, tag, keys): 

437 """ 

438 Load all enabled plugins. 

439 

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

441 be loaded from outside the leo/plugins directory. 

442 """ 

443 

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

445 if not g.unitTesting: 

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

447 

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

449 if not s: 

450 return 

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

452 if 0: 

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

454 g.blue(s2) 

455 for plugin in s.splitlines(): 

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

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

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

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

460 """ 

461 Load one plugin from a file name or module. 

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

463 

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

465 """ 

466 global optional_modules 

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

468 

469 def report(message): 

470 if trace and not g.unitTesting: 

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

472 

473 # Define local helper functions. 

474 #@+others 

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

476 def callInitFunction(result): 

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

478 try: 

479 # Indicate success only if init_result is True. 

480 init_result = result.init() 

481 # Careful: this may throw an exception. 

482 if init_result not in (True, False): 

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

484 if init_result: 

485 self.loadedModules[moduleName] = result 

486 self.loadedModulesFilesDict[moduleName] = ( 

487 g.app.config.enabledPluginsFileName 

488 ) 

489 else: 

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

491 result = None 

492 except Exception: 

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

494 g.es_exception() 

495 result = None 

496 return result 

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

498 def finishImport(result): 

499 """Handle last-minute checks.""" 

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

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

502 if hasattr(result, 'init'): 

503 return callInitFunction(result) 

504 # 

505 # No top-level init function. 

506 if g.unitTesting: 

507 # Do *not* load the module. 

508 self.loadedModules[moduleName] = None 

509 return None 

510 # Guess that the module was loaded correctly. 

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

512 self.loadedModules[moduleName] = result 

513 return result 

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

515 def loadOnePluginHelper(moduleName): 

516 result = None 

517 try: 

518 __import__(moduleName) 

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

520 result = sys.modules[moduleName] 

521 except g.UiTypeException: 

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

523 except ImportError: 

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

525 # except ModuleNotFoundError: 

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

527 except SyntaxError: 

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

529 except Exception: 

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

531 g.es_exception() 

532 return result 

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

534 def reportFailedImport(): 

535 """Report a failed import.""" 

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

537 return 

538 if ( 

539 self.warn_on_failure and 

540 tag == 'open0' and 

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

542 moduleName not in optional_modules 

543 ): 

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

545 #@-others 

546 if not g.app.enablePlugins: 

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

548 return None 

549 if moduleOrFileName.startswith('@'): 

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

551 return None 

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

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

554 moduleName = self.regularizeName(moduleOrFileName) 

555 if self.isLoaded(moduleName): 

556 module = self.loadedModules.get(moduleName) 

557 return module 

558 assert g.app.loadDir 

559 moduleName = g.toUnicode(moduleName) 

560 # 

561 # Try to load the plugin. 

562 try: 

563 self.loadingModuleNameStack.append(moduleName) 

564 result = loadOnePluginHelper(moduleName) 

565 finally: 

566 self.loadingModuleNameStack.pop() 

567 if not result: 

568 if trace: 

569 reportFailedImport() 

570 return None 

571 # 

572 # Last-minute checks. 

573 try: 

574 self.loadingModuleNameStack.append(moduleName) 

575 result = finishImport(result) 

576 finally: 

577 self.loadingModuleNameStack.pop() 

578 if result: 

579 # #1688: Plugins can update globalDirectiveList. 

580 # Recalculate g.directives_pat. 

581 g.update_directives_pat() 

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

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

584 return result 

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

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

587 """Print the plugin signon.""" 

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

589 # in self.loadOnePlugin 

590 m = self.signonModule 

591 if verbose: 

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

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

594 self.signonModule = None # Prevent double signons. 

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

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

597 moduleName = self.regularizeName(moduleOrFileName) 

598 if self.isLoaded(moduleName): 

599 if verbose: 

600 g.pr('unloading', moduleName) 

601 del self.loadedModules[moduleName] 

602 for tag in self.handlers: 

603 bunches = self.handlers.get(tag) 

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

605 self.handlers[tag] = bunches 

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

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

608 def registerExclusiveHandler(self, tags, fn): 

609 """ Register one or more exclusive handlers""" 

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

611 for tag in tags: 

612 self.registerOneExclusiveHandler(tag, fn) 

613 else: 

614 self.registerOneExclusiveHandler(tags, fn) 

615 

616 def registerOneExclusiveHandler(self, tag, fn): 

617 """Register one exclusive handler""" 

618 try: 

619 moduleName = self.loadingModuleNameStack[-1] 

620 except IndexError: 

621 moduleName = '<no module>' 

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

623 if g.unitTesting: 

624 return 

625 if tag in self.handlers: 

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

627 else: 

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

629 self.handlers[tag] = [bunch] # Vitalije 

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

631 def registerHandler(self, tags, fn): 

632 """ Register one or more handlers""" 

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

634 for tag in tags: 

635 self.registerOneHandler(tag, fn) 

636 else: 

637 self.registerOneHandler(tags, fn) 

638 

639 def registerOneHandler(self, tag, fn): 

640 """Register one handler""" 

641 try: 

642 moduleName = self.loadingModuleNameStack[-1] 

643 except IndexError: 

644 moduleName = '<no module>' 

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

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

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

648 if fn not in functions: # Vitalije 

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

650 items.append(bunch) 

651 self.handlers[tag] = items 

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

653 def unregisterHandler(self, tags, fn): 

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

655 for tag in tags: 

656 self.unregisterOneHandler(tag, fn) 

657 else: 

658 self.unregisterOneHandler(tags, fn) 

659 

660 def unregisterOneHandler(self, tag, fn): 

661 bunches = self.handlers.get(tag) 

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

663 self.handlers[tag] = bunches 

664 #@-others 

665#@-others 

666#@@language python 

667#@@tabwidth -4 

668#@@pagewidth 70 

669 

670#@-leo