Python IDE Basics

From The Document Foundation Wiki
Jump to: navigation, search

Preamble

This page presents the steps enabling to develop LibreOffice Python macros within an Integrated Development Environment (IDE). Configuration or preferences setup are presented for a limited set of free and open source (FOSS) IDEs. Direct editing and testing of Python macros within LibreOffice requires solely ::APSO:: extension. In which case Starting, Connecting & Stopping chapter content can be skipped.

( ... Documents, Configuration, Object Inspection ... )

Useful LibreOffice Packs

For those wishing to explore cross-compatibility:

IDE Project Setup

Each IDE requires a different configuration or setup.

What's in a Macro

A LibreOffice Python macro may look like:

import uno
def my_1st_macro():
    # Won't run directly in Anaconda, Geany, KDevelop, PyCharm or else
    doc = XSCRIPTCONTEXT.getDocument()
    doc.getText().setString("Hello World!")

g_exportedScripts = my_1st_macro,

Executing the above requires to bridge together the IDE and LibreOffice. Once done UNO objects become accessible.

Up to five steps can be necessary to achieve this:

  1. Start LibreOffice as a service,
  2. Connect to a service,
  3. create a XSCRIPTCONTEXT Adaptor,
  4. Run the macro,
  5. Stop LibreOffice as a service

While examples available on the internet do not resort to all these steps, their study exhibits coding guidelines that are detailed hereafter. The proposed IDE_utils.py module innocuously integrates in IDEs.

Start, Connect, Adapt, Run & Stop

Below table is listing a few public (Libre|Open)Office Python macros examples. Their usage of the Start, Connect, Adapt, Run, Stop steps is commented. This introduces Coding Guidelines you may apply when using Python IDEs in conjunction with the LibreOffice suite. Identified benefits are underlined.

Example codes
Description Start Connect Adapt Run

Debug

Stop
a. LibreOffice and OpenOffice officehelper.py module x x
b. A bridge for IDE with XSCRIPTCONTEXT revisité, Feb. 2017 (in french) x x x
c. Interface-oriented programming in OpenOffice / LibreOffice: automate your office tasks with Python macros, Sep. 2013 x x
d. LibreOffice macro execution with Pycharm, Jan. 2014 (in japanese) x x x
e. Starting, Stopping and Connecting to OpenOffice with Python, Dec. 2008 x x x x
f. Python UNO-bridge, Oct. 2008 x

Examples Highlights

a. officehelper.py Module

The officehelper.py module, included in LibreOffice and OpenOffice, starts and connects to a LibreOffice instance:

""" Start and connect to (Libre/Open)Office """
import officehelper
ctx = officehelper.bootstrap()

Module help is available from Python interpreter:

>>> import officehelper
>>> help(officehelper)
Help on module officehelper:

bootstrap() help states: The soffice process is started opening a named pipe of random name, then the local context is used to access the pipe. This function directly returns the remote component context, from whereon you can get the ServiceManager by calling getServiceManager() on the returned object.

Starting a service and connecting to it is achieved using Uno-Urls that bridge a Python macro and a running Office instance.

Two different types of connections can be established with LibreOffice:

  • Pipe connections
  • Socket connections

Numerous startup parameters exist in order to run LibreOffice as a service. They can be searched for "Starting LibreOffice Software with parameters" help page as well as online. officehelper module uses two of them: nologo and nodefault.

Notes: The officehelper multi-platform module manages solely pipe connections which may fit most testing needs. Service startup --options are hardwired. Neither stopping means, nor XSCRIPTCONTEXT enablement are present. Observe that extra macro code insertion is necessary.

b. XScriptContext reloaded

""" Please comment this line when running within (Libre/Open)Office """
from Office.Bridge import XSCRIPTCONTEXT

Office.Bridge indicates: The XSCRIPTCONTEXT object facilitates connections to running instances of (Libre/Open)Office. It connects to a piped instance of (Libre/Open)Office named 'LibreOffice'. It connects to a localhost instance of (Libre/Open)Office using port=yyyy (yyyy=current year). It can connect to any variation of pipe name or port# used by a started (Libre/Open)Office instance.

Notes: The Office.Bridge module proposes both pipe and socket hardcoded connections, and introduces a form of connection pooling. Starting and stopping mechanisms are missing. XSCRIPTCONTEXT built-in is supported while code insertion is necessary. Its Python code is unnecessarily verbose.

c. Interface-oriented programming..

import socket  # only needed on win32-OOo3.0.0
import uno

# get the uno component context from the PyUNO runtime
localContext = uno.getComponentContext()

# create the UnoUrlResolver
resolver = localContext.ServiceManager.createInstanceWithContext(
                                "com.sun.star.bridge.UnoUrlResolver", localContext )

# connect to the running office
ctx = resolver.resolve( "uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext" )
smgr = ctx.ServiceManager

# get the central desktop object
desktop = smgr.createInstanceWithContext( "com.sun.star.frame.Desktop",ctx)

# access the current writer document
model = desktop.getCurrentComponent()

Notes: Interface-oriented programming.. solely introduces a hardcoded socket connection. Inserting a few lines is necessary. Despite these facts, that post is a valuable introduction to Python macros.

d. Macro execution with PyCharm

A Python macro may be written and ran as:

def HelloWorld_Writer():
    doc = XSCRIPTCONTEXT.getDocument()
    doc.getText().setString("Hello World!")

if __name__ == "__main__":
    import unopy
    XSCRIPTCONTEXT = unopy.connect()
    HelloWorld_Writer()

Worth mentioning is the use if XSCRIPTCONTEXT Interface Injection, a kind of Dependency Injection design pattern illustrating Inversion of Control. A notable advantage is the if code block that gets processed from any IDE while being ignored from LibreOffice or OpenOffice. Unopy.py module can be copied into <OFFICE>\program directory, and is not required at build time. All in all it's a concise and efficient Adaptor implementation.

The complete module code can be obtained from japanese forums or GitHub:

Notes: The unopy module hardcodes a classical socket connection. Service start/stop are not coded. However its XSCRIPTCONTEXT adaptor is neat, and its impact on macro code is null, zero!

e. Starting, Stopping while Connecting..

import ooutils
oor = ooutils.OORunner()
desktop = oor.connect()
# Do something with the "desktop"

Get Starting, Stopping and Connecting complete code from Linux Journal. Observe OpenOffice startup options presented as list and key-values pairs.

...
def startup(self):
    """
    Start a headless instance of OpenOffice.
    """
    args = [OPENOFFICE_BIN,
            '-accept=socket,host=localhost,port=%d;urp;StarOffice.ServiceManager' % self.port,
            '-norestore',
            '-nofirststartwizard',
            '-nologo',
            '-headless',
            ]
...
    def _shutdown_desktops():
        """ Shutdown all OpenOffice desktops that were started by the program. """
        for port, desktop in _started_desktops.items():
            try:
                if desktop:
                    desktop.terminate()
            except Exception, e:
                pass
...

Notes: The above link exhibits desktop connection pooling while providing a convenient parametrized startup and shutdown of the service. Its use of desktop.terminate() must be noted. This unix-based OpenOffice example, lacks the preceeding XSCRIPTCONTEXT adaptor, and requires extra code insertion.

f. Python UNO-bridge

Notes: Python UNO-bridge demonstrates a common socket connection. Its PyUNO architecture and Python extension coding documentation is worth reading. However start, adapt, run, and stop facilities are absent.

Coding Guidelines

Requirements for Python macro enablement in IDEs may summarize as:

  • start, connect, adapt, run and stop steps to be optional
  • Support multiple platforms i.e. essentially Linux, MacOS and Windows
  • on-demand startup --options
  • Permit pipe and/or socket connections
  • decoupled coding using injection
  • Provide Service pooling, context pooling
  • and KISS

IDE_utils module proposal: A Runner() context manager class is responsible for starting and stopping soffice instances. An optional JSON configuration file contains service-options pairs holding the services to start and their running conditions. A connect() function bridges the actual IDE and LibreOffice instances. A ScriptContext() object is injected as XSCRIPTCONTEXT built-in. start() and stop() functions are coding facilities that wrap-up Runner() features.

#!  # IDE_utils.py
import officehelper
RUNNERS = 'Runners.json'

class Runner(json=None): pass
class ScriptContext(): pass

_ctx = officehelper.bootstrap()
XSCRIPTCONTEXT = ScriptContext(_ctx)

def connect(host='localhost', port=2002, pipe=None): pass
def start(): pass
def stop(): pass

Thus a LibreOffice Python macro library can ressemble these examples:

import uno
def macro_1(): pass  # Your code goes here
def macro_2(): pass  # Your code goes here

g_exportedScripts = macro_1, macro_2  # *Office public macros

#! EXAMPLE 1
if __name__ == "__main__":
    ''' LibreOffice as a Service IDE runnable code''' 
    import IDE_utils as geany
    with geany.Runner() as jesse_owens:  # Start/Stop
        XSCRIPTCONTEXT = geany.XSCRIPTCONTEXT  # Connect/Adapt
        macro_2()  # Run

Example 1: As Runners.json does not exist jesse_owens starts nothing. officehelper opens a named pipe of random name, XSCRIPTCONTEXT injection occurs, macro_2 gets executed and LibreOffice running pipe gets stopped as jesse_owens context manager triggers it.

#! EXAMPLE 2
if __name__ == "__main__":
    import IDE_utils as pycharm 
    with pycharm.Runner(json={pgm: [accept, *options]}) as carl_lewis:  # Start/Stop
        ctx = pycharm.connect(pipe='LibreOffice')
        XSCRIPTCONTEXT = pycharm.ScriptContext(ctx)  # Adapt
        macro_1()  # Run

Example 2: As carl_lewis explores JSON pgm-options dictionary pairs, LibreOffice instances start. A hardwired 'LibreOffice' pipe connection is requested, macro_1 executes and carl_lewis terminates all started instances.
Note: A default injection is performed, in the background, with last pgm-options pair, and overrides officehelper bootstraping.

#! EXAMPLE 3
if __name__ == "__main__":
    import IDE_utils as usain_bolt  # Runner()
    usain_bolt.start()
    ctx = usain_bolt.connect()
    XSCRIPTCONTEXT = usain_bolt.ScriptContext(ctx)  # Adapt
    pass  # Your code goes here
    usain_bolt.stop()

Example 3: usain_bolt starts all services found in optional Runners.json input file, attempts a common socket connection on port 2002, injects an implementation of XSCRIPTCONTEXT built-in. No macro code is called and usain_bolt finally stops all possible (Libre|Open)Office instances.

IDE_utils source code can be obtained from GitLab. IDE_utils module design decisions are drafted below and can be skipped. However IDE_utils Limitations lecture is recommended.

ScriptContext class

ScriptContext objects require an established bridge in order to implement XSCRIPTCONTEXT builtin interface:

  • getComponentContext(), getDestop(), getDocument()
  • getInvocationContext() is not defined
class ScriptContext(XScriptContext):
    def __init__(self, ctx):
        self.ctx = ctx
    def getComponentContext(self):
        return self.ctx
    def getDesktop(self):
        return self.ctx.getServiceManager().createInstanceWithContext("com.sun.star.frame.Desktop", self.ctx)
    def getDocument(self):
        return self.getDesktop().getCurrentComponent()

connect function

Above internet examples demonstrate connection time-out and connection exception handling in various ways.
a. officehelper module performs 20 attempts every half-second before it throws a BootstrapException.
d. Starting, Stopping... connects every second up to six times and raises Exception afterwards.
Observe that NoConnectException must be trapped when connection to LibreOffice.
The IDE_utils connect() function is delegating time-out and exception handling activities to a Python @retry decorator that's responsible for such aspects. Its delays, exception and report arguments may be overwritten using IDE_utils module data variables e.g. RETRY_DELAYS.
All achieved connections aggregate in ScriptContext.pool dictionary according to connect() parameters.
(Libre|Open)Office instances termination are performable out of this pool.

from __future__ import print_function

RETRY_DELAYS = (0, 1, 5, 30, 180, 600, 3600)
RETRY_EXCEPTIONS = NoConnectException
RETRY_REPORT = print

def retry(delays=(0, 1, 5, 30, 180, 600, 3600),
          exception=Exception,
          report=lambda *args: None):
    ...

def connect(host='localhost', port=2002, pipe=None):
    ...
    @retry(delays=RETRY_DELAYS,
           exception=RETRY_EXCEPTIONS,
           report=RETRY_REPORT)  # Python3!
    def resolve():
        return resolver.resolve(uno_url)
    ctx = resolve()
    
    if pipe:
        ScriptContext.pool[pipe] = ctx
    else:
        ScriptContext.pool[(host, port)] = ctx
    return ctx

ScriptContext.pool = {}  # Can be fed by Runner processes too !

Services specification

Services to start and their running conditions are precised within JSON-like dictionary pairs. The expected syntax is {pgm: [accept, *options]} where minimum inputs are the soffice path plus --accept option. Other options appear according to particular use case expectations. LibreOffice and OpenOffice options specification syntax are different.
Single or simultaneous services can be managed.

WIN_libO_x64 = \
    {'C:\Program Files\LibreOffice 5\program\soffice.exe':
     ['--accept="pipe,name=LibreOffice;urp;"',
      '--norestore',
      #'--safe-mode',
      #'--unaccept={UNO-URL}',
      #'--unaccept=all',
      '--language=fr'
      ]
     }
WIN_OOo_x32 = \
    {'F:\OpenOffice.org\App\openoffice\program\soffice.exe':
     ['-accept="socket,host=localhost,port=2002;urp;"',
      '-headless',
      #'-invisible',
      '-nologo',
      '-norestore'
      ]
     }
NIX_aOO = \
    {'/opt/openoffice4/program/soffice':
     ['-accept="pipe,name=ApacheOpenOffice;urp;"']
     }

pgms = {**WIN_libO_x64, **NIX_aOO, **WIN_OOo_x32}  # Python 3!

with Runner(soffice=WIN_libO_x64) as jesse_owens: pass
# LibreOffice single named pipe.
with Runner(soffice=pgms) as carl_lewis: pass
# 3 different *Office versions

Alternatively services can be specified inside the Runners.json file. A different JSON file name can be used, providing IDE_utils.RUNNERS data variable is overwritten. _create_config() function exhibits service specification facilities for JSON files.

Runner class

Runner context manager class makes uses of __enter__ and __exit__ facilities to implement initialisation and termination of *Office services. It starts, stops zero-to-many (Libre|Open)Office processes according to an optional JSON configuration file containing {pgm: [accept, *options]} key-values service pairs.
Besides Runner objects establish service connections for later use.

import IDE_utils as ide

# ex.1
with ide.Runner() as jesse_owens:  # Starts/stops 'soffice' instances
    pass # Your code goes here

# ex.2
task = {'D:\Portable\App\openoffice\program\soffice.exe':
        ['-accept="pipe,name=OpenOffice;urp;"'
         ]  # OpenOffice - Foreground - visible instance
        }
with ide.Runner(soffice=task) as carl_lewis:  # Portable OpenOffice Runner
    pass # Your code goes here

All started services get pooled in obj.processes to be later terminated, while prepared successful connections add to ScriptContext.pool with uno-url keys.

start, stop functions

(Libre|Open)Office services are launched using subprocess standard library Popen function, which permits to stop processes with kill() or terminate() methods. However *Office services create process trees whose branches differ across platforms and products. The above methods do not perfectly interrupt such detached processes under Windows.

Examples:

  • LibreOffice/Windows: soffice.exe detaches soffice.bin
  • Apache OpenOffice/Linux: oosplash detaches soffice

Process termination is performed using *Office API:

def terminate_desktops():
    for ctx in ScriptContext.pool.values():  # {key: ctx} pairs
        ScriptContext(ctx).getDesktop().terminate()


Utilities

create_config() and killall_soffice() utility functions respectively permit to build a JSON config file and to stop all remnants of *Office started services

killall_soffice() function can interrupt LibreOffice instances as shown:

import subprocess, sys
if sys.platform.startswith('linux') or sys.platform == 'darwin':
    subprocess.Popen(['killall','soffice'])
elif sys.platform.startswith('win'):
    subprocess.Popen(['taskkill','/f','/t', '/im','soffice'])


Help - ...
To be or not to Be UI ( Headless, invisible, ... )

Improvement Tracks, Limitations

Start/Stop remote instances, all platforms

Stop Runner.processes i.o. all, all platforms

Time-out tuning

Managing Documents

Opening a Document

Creating a Document

Saving a Document

Closing a Document

A General Purpose Converter

Bug Detection & Reporting

Examining

Examining LibreOffice

Configuration Properties

Path Settings

Getting & Setting DocumentProperties

Reporting OS File Properties

Setting Document Properties

Inspecting a Document for API Information

MRI, xRay & ObjectInspector


Listening

... catching document(s) events, catching URP bridge events...

import uno, msgbox
def listening():
    pass  # Your code goes here

g_exportedScripts = listening,
import msgbox
help(msgbox)