Executing 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.
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.
startup macros
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.