Embedding Leo with the leoBridge module

The leoBridge module allows complete access to all aspects of Leo from other Python programs running independently of Leo. Let us call such a program a host program. Using the leoBridge module, host programs can get access to:

  • all of Leo’s source code,
  • the contents of any .leo file,
  • the commander of any .leo file.

The basics

Host programs use the leoBridge module as follows:

import leo.core.leoBridge as leoBridge
controller = leoBridge.controller(gui='nullGui')
g = controller.globals()
c = controller.openLeoFile(path)

Let us look at these statements in detail. The statements:

import leo.core.leoBridge as leoBridge
controller = leoBridge.controller(gui='nullGui')

import the leoBridge module and create a bridge controller. In effect, these statements embed an invisible copy of Leo into the host program. This embedded copy of Leo uses a null gui, which simulates all aspects of Leo’s normal gui code without creating any screen objects.

The statement:

g = controller.globals()

provides access to Leo’s leoGlobals module, and properly inits globals such as g.app, g.app.gui, etc. Host programs should not import leoGlobals directly, because doing so would not init the g.app object properly.

The statement:

c = controller.openLeoFile(path)

invisibly opens the .leo file given by the path argument. This call returns a completely standard Leo commander, properly inited. This is the big payoff from the leoBridge module: the host program gets instant access to c.config.getBool, etc. Do you see how sweet this is?

For example, the following script runs leo/test/leoBridgeTest.py outside of Leo. leoBridgeTest.py uses the leoBridge module to run all unit tests in leo/test/unitTest.leo:

import os,sys

path = g.os_path_abspath(
    g.os_path_join(
        g.app.loadDir,'..','test','leoBridgeTest.py'))

os.system('%s %s' % (sys.executable,path))

The file leo/test/test.leo contains the source code for leoBridgeTest.py. Here it is, stripped of its sentinel lines:

'''A program to run unit tests with the leoBridge module.'''

import leo.core.leoBridge as leoBridge
import leo.core.leoTest as leoTest

def main ():
    tag = 'leoTestBridge'

    # Setting verbose=True prints messages that would be sent to the log pane.
    bridge = leoBridge.controller(gui='nullGui',verbose=False)
    if bridge.isOpen():
        g = bridge.globals()
        path = g.os_path_abspath(g.os_path_join(
            g.app.loadDir,'..','test','unitTest.leo'))
        c = bridge.openLeoFile(path)
        g.es('%s %s' % (tag,c.shortFileName()))
        runUnitTests(c,g)

    print tag,'done'

def runUnitTests (c,g):
    nodeName = 'All unit tests' # The tests to run.
    try:
        u = leoTest.testUtils(c)
        p = u.findNodeAnywhere(nodeName)
        if p:
            g.es('running unit tests in %s...' % nodeName)
            c.selectPosition(p)
            c.debugCommands.runUnitTests()
            g.es('unit tests complete')
        else:
            g.es('node not found:' % nodeName)
    except Exception:
        g.es('unexpected exception')
        g.es_exception()
        raise

if __name__ == '__main__':
    main()

Running leoBridge from within Leo

This following is adapted from Terry Brown’s entry in Leo’s wiki.

You can not just run leoBridge from Leo, because the leoBridge module is designed to run a separate copy of Leo. However, it is possible to run leoBridge from a separate process. That turned out to be more, um, interesting than anticipated, so I’m recording the results here.

The idea is that script A running in Leo (i.e. in a regular GUI Leo session) calls script B through subprocess.Popen(), script B uses LeoBridge to do something (parse unloaded Leo files), and returns the result to script A. Passing the result back via the clipboard seemed like a possibility, but XWindows / tcl/tk clipboard madness being what it is, that didn’t seem to work.

First trick, calling script B from script A:

import subprocess
p = subprocess.Popen(('python',
    path_to_script_B,
    parameter_for_script_B,),
    stdout=subprocess.PIPE,
    env={'PYTHONPATH': g.app.loadDir,'USER': g.app.leoID},
)
p.wait()

Setting PYTHONPATH in the environment seemed like the easiest way to let script B find leoBridge.py (which it needs to import). But by setting the env parameter you limit script B’s environment to be only PYTHONPATH, which causes leoBridge to fail because, in unix at least, it depends on USER in the environment. So you need to pass that through, too.

Now, because passing stuff back on the clipboard seems unreliable, at least in XWindows, script B passes results back to script A via stdout (print), but there’s some Leo initialization chatter you want to avoid. So put a sentinel, ‘START_CLIPBOARD’, in the output, and collect it like this:

response = p.stdout.readlines()
while response and 'START_CLIPBOARD' not in response[0]:
    del response[0]
del response[0]  # delete the sentinel as well
response = ''.join(response)

This is the basic mechanism. What I actually wanted to do was have script B generate a branch of nodes and pass that back to script A for insertion in the tree script A is running in. That’s relatively easy if you use:

c.setCurrentPosition(pos_of_branch_to_return)
c.copyOutline()
print '<!-- START_CLIPBOARD -->'
print g.app.gui.getTextFromClipboard()
print '<!-- END_CLIPBOARD -->'

at the end of script B. Back in script A, after you’ve rebuilt response as shown above, do:

g.app.gui.replaceClipboardWith(response)
c.pasteOutline()

Previous topic

IPython and Leo

Next topic

Using Vim Bindings with Leo