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

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()
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.
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 """
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
40 Usage: instantiate, execute "add" to add commands (with optional
41 priority), execute normally via f() calling mechanism.
43 """
45 def __init__(self, commands=None):
46 if commands is None:
47 self.chain = []
48 else:
49 self.chain = commands
51 def __call__(self, *args, **kw):
52 """ Command chain is called just like normal func.
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
69 def __str__(self):
70 return str(self.chain)
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])
77 def __iter__(self):
78 """ Return all objects in chain.
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
89 .. contents::
91 Usage
92 =====
94 Initialization
95 --------------
97 - import the base class::
99 from leoPlugins from leo.core import leoBasePlugin
101 - create a class which inherits from leoBasePlugin::
103 class myPlugin(leoBasePlugin):
105 - in the __init__ method of the class, call the parent constructor::
107 def __init__(self, tag, keywords):
108 super().__init__(tag, keywords)
110 - put the actual plugin code into a method; for this example, the work
111 is done by myPlugin.handler()
113 - put the class in a file which lives in the <LeoDir>/plugins directory
114 for this example it is named myPlugin.py
116 - add code to register the plugin::
118 leoPlugins.registerHandler("after-create-leo-frame", Hello)
120 Configuration
121 -------------
123 BaseLeoPlugins has 3 *methods* for setting commands
125 - setCommand::
127 def setCommand(self, commandName, handler,
128 shortcut = None, pane = 'all', verbose = True):
130 - setMenuItem::
132 def setMenuItem(self, menu, commandName = None, handler = None):
134 - setButton::
136 def setButton(self, buttonText = None, commandName = None, color = None):
138 *variables*
140 :commandName: the string typed into minibuffer to execute the ``handler``
142 :handler: the method in the class which actually does the work
144 :shortcut: the key combination to activate the command
146 :menu: a string designating on of the menus ('File', Edit', 'Outline', ...)
148 :buttonText: the text to put on the button if one is being created.
150 Example
151 =======
153 Contents of file ``<LeoDir>/plugins/hello.py``::
155 class Hello(BaseLeoPlugin):
156 def __init__(self, tag, keywords):
158 # call parent __init__
159 super().__init__(tag, keywords)
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')
167 # create a command with a shortcut
168 self.setCommand('Hola', self.hola, 'Alt-Ctrl-H')
170 # create a button using different text than commandName
171 self.setButton('Hello in Spanish')
173 # create a menu item with default text
174 self.setMenuItem('Cmds')
176 # define a command using setMenuItem
177 self.setMenuItem('Cmds', 'Ciao baby', self.ciao)
179 def hello(self, event):
180 g.pr("hello from node %s" % self.c.p.h)
182 def hola(self, event):
183 g.pr("hola from node %s" % self.c.p.h)
185 def ciao(self, event):
186 g.pr("ciao baby (%s)" % self.c.p.h)
188 leoPlugins.registerHandler("after-create-leo-frame", Hello)
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):
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()
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)
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.
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.
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.
440 Using a module name (without the trailing .py) allows a plugin to
441 be loaded from outside the leo/plugins directory.
442 """
444 def pr(*args, **keys):
445 if not g.unitTesting:
446 g.es_print(*args, **keys)
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.
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
469 def report(message):
470 if trace and not g.unitTesting:
471 g.es_print(f"loadOnePlugin: {message}")
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)
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)
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)
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
670#@-leo