Before we can start defining what we allow our users to do to Kalam, we need to build the core macro execution functionality. The first step is to make sure users can execute the contents of a document.
Unless you have skipped all previous occasions of creating and adding an action to the menubar and the toolbar, you will know by now how to do so. I have supplied the slot code that executes the contents of the currently active document. You will find the complete code in the kalamapp.py file that belongs to this chapter.
# kalamapp.py
...
class KalamApp(QMainWindow):
...
# Macro slots
...
def slotMacroExecuteDocument(self):
if self.docManager.activeDocument() == None:
QMessageBox.critical(self,
"Kalam",
"No document to execute as a macro ")
return
try:
bytecode = compile(str(self.docManager.activeDocument().text()),
"<string>",
"exec")
except Exception, e:
QMessageBox.critical(self,
"Kalam",
"Could not compile " +
self.docManager.activeDocument().title() +
"\n" + str(e))
try:
exec bytecode # Note: we don't yet have a separate namespace
# for macro execution
except Exception, e:
QMessageBox.critical(self,
"Kalam",
"Error executing " +
self.docManager.activeDocument().title() +
"\n" + str(e))
...
We are being a bit careful here, and thus compile the code first to check for syntax errors. These, along with execution errors, will be shown in a dialog box. Note that anything you print from here will go to standard output—that is, a black hole if you run Kalam by activating an icon, or the terminal window if you run Kalam from the shell prompt. It would be a logical step to redirect any output to a fresh Kalam document (this is what Emacs does). It is quite easy to achieve. You can reassign the standard and error output channels to any object you want, as long as it has a write() function that accepts a string. We might want to add a write() function to KalamDoc.
The implementation of write() in KalamDoc is very simple:
# kalamdoc.py - fragment
...
def write(self, text, view = None):
self.text().append(text)
self.emit(PYSIGNAL("sigDocTextChanged"),
(self._text, view))
Having done that, redirecting all output is easy:
...
def slotMacroExecuteDocument(self):
...
import sys
document = self.docManager.createDocument(KalamDoc, KalamView)
document.setTitle("Output of " + title)
oldstdout = sys.stdout
oldstderr = sys.stderr
sys.stdout = document
sys.stderr = document
exec bytecode # Note: we don't yet have a separate namespace
# for macro execution
sys.stdout = oldstdout
sys.stderr = oldstderr
...
It is necessary to save the "real" standard output and standard error channels in order to be able to restore them when we are done printing to the output document. Otherwise all output, from anywhere inside Kalam, would go forever to that document, with nasty consequences if the user were to remove the document.
Until we create a namespace specially for executing macros, everything runs locally to the function that executes the macro. That is, you can use self to refer to the current instance of KalamApp.
Executing a bit of code from a document.
Of course, littering the KalamApp with macro execution code isn't the best of ideas. This leads us to the creation of a macro manager class, MacroManager, which keeps a dictionary of compiled code objects that can be executed at will. I won't show the unit tests here: it is available with the full source code.
"""
macromanager.py - manager class for macro administration and execution
copyright: (C) 2001, Boudewijn Rempt
email: boud@rempt.xs4all.nl
"""
from qt import *
import sys
class MacroError(Exception):pass
class NoSuchMacroError(MacroError):
def __init__(self, macro):
ERR = "Macro %s is not installed"
self.errorMessage = ERR % (macro)
def __repr__(self):
return self.errorMessage
def __str__(self):
return self.errorMessage
class CompilationError(MacroError):
def __init__(self, macro, error):
ERR = "Macro %s could not be compiled. Reason: %s"
self.errorMessage = ERR % (macro, str(error))
self.compilationError = error
def __repr__(self):
return self.errorMessage
def __str__(self):
return self.errorMessage
class ExecutionError(MacroError):
def __init__(self, error):
ERR = "Macro could not be executed. Reason: %s"
self.errorMessage = ERR % (str(error))
self.executionError = error
def __repr__(self):
return self.errorMessage
def __str__(self):
return self.errorMessage
First, a couple of exceptions are defined. We want to separate the GUI handling of problems with the macro from the actual execution, so that whenever something goes wrong, an exception is thrown.
class MacroAction(QAction):
def __init__(self, code, *args):
apply(QAction.__init__,(self,) + args)
self.code = code
self.bytecode = self.__compile(code)
self.locations=[]
self.connect(self,
SIGNAL("activated()"),
self.activated)
def activated(self):
self.emit(PYSIGNAL("activated"),(self,))
def addTo(self, widget):
apply(QAction.addTo,(self, widget))
self.locations.append(widget)
def removeFrom(self, widget):
QAction.removeFrom(self, widget)
del self.locations[widget]
def remove(self):
for widget in self.locations:
self.removeFrom(widget)
def __compile(self, code):
try:
bytecode = compile(code,
"<string>",
"exec")
return bytecode
except Exception, e:
raise CompilationError(macroName, e)
def execute(self, out, err, globals, locals):
try:
oldstdout = sys.stdout
oldstderr = sys.stderr
sys.stdout = out
sys.stderr = err
exec self.bytecode in globals
sys.stdout = oldstdout
sys.stderr = oldstderr
except Exception, e:
print e
print sys.exc_info
sys.stdout = oldstdout
sys.stderr = oldstderr
raise ExecutionError(e)
By encapsulating each macro in a QAction, it will become very easy to assign shortcut keys, menu items and toolbar buttons to a macro.
The MacroAction class also takes care of compilation and execution. The environment, consisting of the globals and locals dictionaries, is passed in the execute() function. We also pass two objects that replace the standard output and standard error objects. These can be Kalam documents, for instance. Note how we carefully restore the standard output and standard error channels. The output of the print statement in the exception clause will go to the redefined channel (in this instance, the kalam document).
class MacroManager(QObject):
def __init__(self, parent = None, g = None, l = None, *args):
""" Creates an instance of the MacroManager.
Arguments:
g = dictionary that will be used for the global namespace
l = dictionary that will be used for the local namespace
"""
apply(QObject.__init__,(self, parent,) + args)
self.macroObjects = {}
if g == None:
self.globals = globals()
else:
self.globals = g
if l == None:
self.locals = locals()
else:
self.locals = l
All macros should be executed in the same environment, which is why the macromanager can be constructed with a globals and a locals environment. This environment will be used later to create a special API for the macro execution environment, and it will include access to the window (i.e. the KalamApp object) and to the document objects (via the DocManager object).
def deleteMacro(self, macroName):
del self.macroObjects[macroName]
def addMacro(self, macroName, macroString):
action = MacroAction(macroString, self.parent())
self.macroObjects[macroName] = action
self.connect(action,
PYSIGNAL("activated"),
self.executeAction)
return action
def executeAction(self, action):
action.execute(sys.stdout,
sys.stderr,
self.globals,
self.locals)
The rest of the MacroManager class is simple, including methods to delete and add macros, and to execute a named macro. Note how the activated signal of the MacroAction is connected to the executeAction slot. This slot then calls execute() on the macro action with standard output and standard error as default output channels. A macro can, of course, create a new document and divert output to that document.
The MacroManager is instantiated as part of the startup process of the main application:
# kalamapp.py
def initMacroManager(self):
g=globals()
self.macroManager = MacroManager(self, g)
Initializing the macromanager will also entail deciding upon a good API for the macro extensions. This will be covered in a later section.
Adapting the slotMacroExecuteDocument() slot function to use the MacroManager is quite straightforward:
# kalamapp.py
def slotMacroExecuteDocument(self):
if self.docManager.activeDocument() == None:
QMessageBox.critical(self,
"Kalam",
"No document to execute as a macro ")
return
title = self.docManager.activeDocument().title()
try:
macroText = str(self.docManager.activeDocument().text())
self.macroManager.addMacro(title, macroText)
except CompilationError, e:
QMessageBox.critical(self,
"Kalam",
"Could not compile " +
self.docManager.activeDocument().title() +
"\n" + str(e))
return
try:
doc, view = self.docManager.createDocument(KalamDoc, KalamView)
doc.setTitle("Output of " + title)
self.macroManager.executeMacro(title, doc, doc)
except NoSuchMacroError, e:
QMessageBox.critical(self,
"Kalam",
"Error: could not find execution code.")
except ExecutionError, e:
QMessageBox.critical(self,
"Kalam",
"Error executing " + title +
"\n" + str(e))
except Exception, e:
QMessageBox.critical(self,
"Kalam",
"Unpleasant error %s when trying to run %s." \
% (str(e), title))
Note the careful handling of exceptions. You don't want your application to crash or become unstable because of a silly error in a macro.
Executing the contents of a document is very powerful in itself—especially since we have access to the complete KalamApp object, from which we can reach the most outlying reaches of Kalam.
It would be very unpleasant for a user to have to load his macros as a document every time he wants to execute a macro. Ideally, a user should be able to define a set of macros that run at start-up, and be able to add macros to menu options and the keyboard.
Solving the first problem takes care of many other problems in one go. Users who are capable of creating macros are probably able to create a startup macro script that loads all their favorite macros.
We define two keys in the configuration file, macrodir and startupscript. These are the name and location of the Python script that is executed when Kalam is started. We start a user macro after all standard initialization is complete:
# kalamapp.py - fragment
...
class KalamApp(QMainWindow):
def __init__(self, *args):
apply(QMainWindow.__init__,(self, ) + args)
...
# Run the startup macro script
self.runStartup()
...
def runStartup(self):
"""Run a Python script using the macro manager. Which script is
run is defined in the configuration variables macrodir and startup.
All output, and eventual failures are shown on the command-line.
"""
try:
startupScript = os.path.join(kalamconfig.get("macrodir"),
kalamconfig.get("startupscript"))
startup = open(startupScript).read()
self.macroManager.addMacro("startup", startup)
self.macroManager.executeMacro("startup")
except Exception, e:
print "Could not execute startup macro", e
A sample startup script might start Kalam with an empty document:
# startup.py - startup script for Kalam
print "Kalam startup macro file"
self.docManager.createDocument(KalamDoc, KalamView)
It is already possible to do anything you want using these macro extensions, but life can be made easier by providing shortcut functions: a special macro API. We will create one in the next section. However, a serious macro writer would have to buy a copy of this book in order to be able to use all functionality, because hiding the underlying GUI toolkit would remove far too much power from his hands.