Working with configuration settings can be
divided into two main procedures: giving your application
classes access to the configuration data, and loading and saving
that data. We'll start by looking at the first problem, and then
at loading and saving. In the next chapter, we'll round out
Kalam by creating a preferences
dialog.
Handling configuration data in your
application
Before we start saving and restoring
configuration settings, we should have a clear idea of how to
handle them in the application. Configuration data typically
must be available everywhere in the application, because all
objects must be able to query and store settings at
will.
In other languages, such as Visual
Basic, you would use a module with global variables to store
configuration data; in a language like Java or C++, you would
use a singleton object—that is, an object with a hidden
constructor that can only be instantiated once. Python,
however, does not support these constructions.
Of course, there is an alternative. In a sense,
class definitions are global. Every module that imports a
certain class gets exactly the same class. Keep in mind that a
class is just an object, of the type
class. You can associate variables not only
with an object, as in:
class SomeClass:
def __init__(self):
self.someVariable=1
someInstance=SomeClass()
print someInstance.someVariable
But also with a class:
class SomeClass:
classVariable=1
print SomeClass.classVariable
These class variables are accessed via the
name of the class, instead of the name of
an instance of that class. Class variables are shared by all
instances of a class.
The ideal solution to creating a
"global" configuration repository is to define a class that
contains all configuration data as class variables. It's also
possible to encapsulate the configuration data repository in a
single class variable. You cannot call functions on a
class - there is no equivalent to the ‘static' methods of
Java. If we need functions to work on the configuration data,
we must either define those functions at module level, or as
functions of an object that is a class variable of the
configuration module. An example would be a function to create
a QFont out of a fontname
string.
Well — that was the theory. Let's
now look at the code needed to implement configuration data
for Kalam. It's pretty similar to
the snippets we saw above:
"""
kalamconfig.py - Configuration class for the Kalam Unicode Editor
copyright: (C) 2001, Boudewijn Rempt
email: boud@rempt.xs4all.nl
"""
import sys, os
from qt import *
class Config:
APPNAME = "kalam"
APPVERSION = "ch13"
CONFIGFILE = ".kalam-ch13"
currentStyle="Platinum"
viewmanager="tabmanager"
app_x=0
app_y=0
app_w=640
app_h=420
fontfamily="courier"
pointsize=12
weight=50
italic=0
encoding=22
def getApplicationFont():
return QFont(Config.fontfamily,
Config.pointsize,
Config.weight,
Config.italic,
Config.encoding )
As you can see, it's just a simple matter of a class with a bunch
of class variables that represent pertinent values.
However,
because these values will be saved to a file, you cannot
associate real objects with the keys. To make it easier to
retrieve a font based on the values stored in
the configuration file, there is module-level helper function,
getApplicationFont(), which constructs a
QFont on the fly.
As you can see, we store our settings in
a flat namespace, in which every key must be unique. This is
just like the properties system used in Java, but more complex
systems can be very useful. For instance, the Windows registry
is one gigantic tree, and even the files created by
ConfigParser have sections and
subsections. For highly complex configuration needs, there is
the shlex Python module, which you can
use to define configuration languages.
Saving and loading the configuration data
Retrieving and saving the configuration
data can be made as complex or easy as you want. We have
already discussed the possibility of using
_winreg or
ConfigParser for the saving and
retrieving of configuration data.
What we are going to, however, is far
more simple. When we load the settings, we just read every
line in the configuration file, and add a variable to the
Config class that represents the
value:
def readConfig(configClass = Config):
sys.stderr.write( "Initializing configuration\n")
try:
for line in open(os.path.join(os.environ["HOME"],
Config.CONFIGFILE)).readlines():
k, v=tuple(line.split("="))
v=v[:-1]
if v=="None\n":
v=None
elif type:
try:
v=int(v)
except ValueError:
pass
setattr(configClass, k, v)
except IOError:
sys.stderr.write( "Creating first time configuration\n")
To add the variable to the
Config we use the standard Python
function setattr() — this function
is one of the delights that make Python so dynamic.
Note the special treatment of the value
that is represented by "None" in the configuration file: if
"None" is encountered the value of the configuration key is
set to a real None object. This contrast with the situation
where the value is simply empty: then the value is set to an
empty string ("").
Currently, the configuration file format
only supports two types: strings and integers. The distinction
is made by brute force: we simply try to convert the value to
an integer, and if we succeed, it stays an integer. If the
conversion raises a ValueError, we assume
the value should remain a string.
By now you might be wondering
when we will be reading in the
configuration values. The simple answer is that we will do so
when the KalamConfig module is first
imported. At the bottom of the module the function
readConfig(Config) is called, and is only
executed once:
readConfig()
Saving the configuration values to disk
is a simple matter of looping over the contents of the
attributes of the Config class —
that is, the __dict__,
__methods__ and
__members__ dictionaries that are part of
the object's hidden attributes. We retrieve these with the
dir() function:
def writeConfig(configClass = Config):
sys.stderr.write( "Saving configuration\n")
configFile=open(os.path.join(os.environ["HOME"],".kalamrc"),"w+")
for key in dir(Config):
if key[:2]!='__':
val=getattr(Config, key)
if val==None or val=="None":
line=str(key) + "=\n"
else:
line=str(key) + "=" + str(val) + "\n"
configFile.write(line)
configFile.flush()
The actual values are retrieved with
the opposite of setattr():
getattr(). As a first check, attributes
with a double underscore as prefix are not saved: those are
internal attributes to the Config
class. If the value is the None object, we
print the string "None". Because it is quite possible that
some values are QString objects, and
because you cannot save these, everything is converted to a plain
Python string.
Finally, you might need functions that get and set more
complex objects in the Config. These can
be simple module level functions that work on the class:
By now we have a simple configuration
data mechanism, and it's time to use it. Earlier we defined a
few settings: the position and size of the application window,
the widget style that is to be used, and the interface
paradigm. First, we will write some code to actually use these
settings. Then we will write code to save changes when the
application is closed.
Font settings
The font to be used in the editor window can be
set and retrieved with the get and set functions we defined
above. The KalamView class is the place
to use this setting.
"""
from qt import *
import kalamconfig
from resources import TRUE, FALSE
class KalamView(QWidget):
def __init__(self, parent, doc, *args):
apply(QWidget.__init__,(self, parent) + args)
...
self.editor=QMultiLineEdit(self)
self.editor.setFont(kalamconfig.getTextFont())
self.layout.addWidget(self.editor)
We import the configuration module,
not the Config
class from the configuration module. After creating the
editor widget, we simply set the font with a call to
self.editor.setFont(kalamconfig.getTextFont()).
Window geometry
Applying the geometry is just as easy.
It's very pleasant for users when an application pops up its
windows at the same place and in the same size as the user
left them. This is part of session management, which is very
advanced in the KDE environment, but less so for Windows. Qt
3 offers support for session management with
QSessionManager and
QApplication, but we'll take care of
session management ourselves at this time.
Setting the correct size and position
of a window, and also the correct widget style, is done in
the central application object,
KalamApp:
from qt import *
...
import kalamconfig
...
class KalamApp(QMainWindow):
"""KalamApp is the toplevel application window of the kalam unicode editor
application.
"""
def __init__(self, *args):
apply(QMainWindow.__init__,(self, ) + args)
...
self.initSettings()
...
#
# GUI initialization
#
def initSettings(self):
qApp.setStyle(kalamconfig.getStyle())
self.setGeometry(kalamconfig.Config.app_x,
kalamconfig.Config.app_y,
kalamconfig.Config.app_w,
kalamconfig.Config.app_h)
Here, too, we import the
kalamconfig module. The function
initSettings() is called from the
constructor {__init__()}
This function will be extended with
other application level settings during development of
Kalam.
Determining the widget style
First, we set the desired widget
style. Users can also set the widget style using a
command-line option, and Qt can even figure out which style
fits best with a users platform. But some people have strong
preferences, and will want to configure their preferred
style. It is easy enough to determine and use the platform
default if no special style is set.
The getStyle() and
setStyle are quite interesting, from a
Python point of view:
I wanted this to be as flexible as
possible, showing the dynamic nature of Python. The
__extractStyle function takes the
current style object that is used by the application. We
find this by calling qApp.style().
qApp is a global variable that points to
the QApplication object.
An instance in Python has a number of
‘hidden' fields and methods that each have a special
meaning. One of these is __init__(),
which is called when the object is first created. Another is
__class__, which returns the class that
the object was created from. You can use this to make more
instances, but in this case
we are interested in the string that contains the name of
the class. You can retrieve the name with another ‘hidden'
variable of
the class class:
__name__.
Setting the style in the context of
kalamconfig means setting the
"currentStyle" attribute of Config to a
string that represents the style. If the input to
setStyle() is already a string (that is, if the type
is types.StringType), then we simply set
it. Otherwise, we use the function defined above to get a string
that equals the name of the style class.
def getStyle():
# Basic sanity check - you don't want to eval arbitrary code
if not hasattr(Config, "currentStyle"):
print "ok", repr(qApp.style())
Config.currentStyle = __extractStyle(qApp.style())
if (Config.currentStyle[0] != "Q" or
Config.currentStyle[-5:] != "Style" or
Config.currentStyle.find(" ") > 0):
Config.currentStyle = "QPlatinumStyle"
try:
# you shouldn't use eval for this, but it is a nice opportunity
# for showing how it works. Normally you'd use a dictionary of
# style names.
return eval(Config.currentStyle)()
except NameError, e:
print "No such style: defaulting to Platinum"
return QPlatinumStyle()
Getting a QStyle
object of the right type is a bit more complex. Of course,
you will most often use a simple dictionary that maps style
names to style classes:
styleDict = { "platinum": QPlatinumStyle, ...}
This is not particularly flexible. Here,
we use eval to create an object from
the name of a class. Look carefully at:
return eval(Config.currentStyle)()
This means that, if the variable
Config.currentStyle contains a string
that is equal to classname and that is known to Python (that is,
it can be found in one of the imported modules),
eval() will return that class. The
brackets after eval make an instance of the class.
Beware: using
eval is dangerous.
For example, what if someone hacked your
.kalam-ch13 configuration file and set
the entry currentStyle to
os.rmdir('/')? If you were fool enough to
run Kalam as root on Unix, you'd
lose your system—irretrievably.
This is why I checked the existence
and believability of the currentStyle
string before
eval-ing it. I only used
eval to show you that it exists—
for your own sake, don't use eval
trivially! We'll return to eval and its
friends in Chapter 20.
Setting the viewmanager
The last task we handle in this
chapter is the choosing of the view manager. The available
choices include tabbed windows, mini-windows, splitters,
stacks — the lot. This time, we will use a dictionary
that maps viewmanager names to actual classes. This is only
to show you how it works - in general, it's a good rule to
not mix and match approaches as we have done here, but to
choose one method, and stick to it.
First, a dictionary
(workspacesDictionary) is created that
contains a mapping from strings to the actual classes. Of
course, in order to be able to access those classes, they
will have to be imported.
These two functions get and set the
viewmanager style. If the style given in
Config doesn't exist, a
KeyError will be raised, in which case we
simply return a sensible default.
The
getViewManager() is called from the
initWorkSpace() function in
kalamapp.py:
The configuration should be written to a file
when the app closes. There are two places where
Kalam can end:
slotFileQuit(), and in the eventhandler
eventFilter().