The PEAK Developers' Center   HelloWorld UserPreferences
 
HelpContents Search Diffs Info Edit Subscribe XML Print View
Version as of 2003-12-06 11:22:13

Clear message


By R. David Murray (rdmurray at bitdance.com) (PJE has edited Chapter 1)

There's a long tradition of teaching programmers how to use programming systems via "Hello, World!" examples. Why should PEAK be left out? I'm going to try to develop a series of stripped-to-the-essentials examples here that provide complete working PEAK programs that demonstrate some of the basic services provided by PEAK. Don't expect to find deep knowledge here; this article is intended to help you get over the first hump of using PEAK.

(Note to Windows users: if you're not using a Unix-like shell (e.g. Cygwin/Bash), you'll need to change the command lines used here. For example, peak foo bar will become something like c:\Python22\Python.exe c:\Python22\peak foo bar. None of the #! lines will work for you, either, and to set environment variables like PYTHONPATH, you'll need to use a separate SET command. However, there's nothing Unix-specific about the framework or examples, and your applications will still run on Windows. It's just that the standard Windows shell doesn't offer as many conveniences in these areas. If you have trouble running the examples on Windows, write up your question on the PEAK mailing list, and we'll try to get these examples updated with more Windows-specific command lines.)

1 Basic Configuration

1.1 Introduction

At base, a PEAK program is a Python program. PEAK is a toolkit and framework, which means it provides stuff you can use in your Python programs to get things done more easily and more quickly, and usually more elegantly.

So, the simplest PEAK "Hello, world!" program is also, in a trivial sense, the Python "Hello, world!" program:

    1     print "Hello, World!"
This, however, is very uninteresting.

One of the things PEAK provides is a framework for giving application programs easy access to their associated configuration data. The goal of this first chapter will be to show how to turn the output of the simple "Hello, world!" program into a configured string. I'm going to get us there step by step, keeping the program in a runnable state at each step (except the first!)

1.2 Running helloworld under PEAK

Since we want to use the configuration framework, our trivially simple Python program needs to be complexified by adding a configuration file along side the Python code file containing the actual print statement. That configuration file looks like this:

 
    [peak.running] 
 
    app = importString("helloworld:HelloWorld") 
 
What this is doing is defining a configuration item named peak.running.app, and setting it to the results of calling the PEAK importString function on the string "helloworld:HelloWorld". The peak.running package provides various services for running application programs.

For convenience in starting PEAK applications, the peak.running package provides a peak command-line script, that is installed alongside Python when you install PEAK. We'll use the peak script to invoke our application, by telling it to run our helloworld configuration file. If we put the above text into a file named helloworld, we would call it using the peak command as follows:

 
    peak runIni helloworld 
 
At the moment, this will result in nothing but some error messages. To make a working application, we need to actually provide the pieces we have declared through the configuration file.

The peak runIni command expects the peak.running.app configuration property to supply a Python object that provides one of several possible interfaces. The simplist interface an application object can provide is just to be a callable with no parameters.

So, our helloworld.py file needs to contain a Python object that when called with no parameters prints "Hello, world!". That's simple enough to arrange, and hardly more complicated than the trivial python "Hello, world" example:

    1     def HelloWorld():
    2         print "Hello, world!"
Our PEAK application is now runnable.

The importString() function acts like a Python import statement, such that with the importString("x:y") is equivalent to from x import y. Thus, the importString() line of the config file shown above is roughly equivalent to:

 
    from helloworld import HelloWorld as app 
 
You will note that because we are importing from a Python module, the module is going to have to be on the PYTHONPATH.

If we've got our two files (helloworld and helloworld.py) in the current directory, we can execute this working PEAK program as follows:

 
    PYTHONPATH=. peak runIni helloworld 
 
Just as a side note, if a basic (callable) app object like this returns a value, that value has significance. If the value is an integer (or None), it will be used as the return code from the peak script, and of our command line as a whole. If it is not an integer, it will be echoed to the terminal, and 1 will be used as the return code from the peak script.

1.3 Execution via Command Name

The configuration file for a complex application is central to that application. Under Unix-like command shells, PEAK allows configuration files to be the "executable" file of the application, the file whose name is entered at the console to invoke the program. This is done using the "magic cookie" of the unix shell:

 
    #!/usr/bin/env peak runIni 
 
    [peak.running] 
 
    app = importString('helloworld:HellowWorld') 
 
This uses the Unix utility env to find the peak command in the current PATH, and calls it. peak then parses the file just like it does when called via peak runIni [file] from the command line.

PEAK is still looking for the modules to import in the PYTHONPATH, though, so you have to call this helloworld like this:

 
    PYTHONPATH=. ./helloworld 
 
(Don't forget to chown +x helloworld first if you're on Unix).

In a real application, you could arrange for your modules to be in a package on the system PYTHONPATH, thus enabling your command to simply be called by name.

If you can't arrange for the package to be on the system PYTHONPATH, or you don't want it to be there, you can instead write a little shell script. In this case you might call the config file helloworld.ini. If helloworld.ini and helloworld.py are in the same directory as the helloworld script, the script might look like this:

 
    #!/bin/sh 
    here=`dirname $0` 
    PYTHONPATH=$here peak runIni $here/helloworld.ini 
 
Of course, in a real app you'd probably put the script in your /usr/local/bin directory or equivalent and the module files in some special directory, and replace $here with the actual path to those module files.

1.4 Application Configuration

Now we'll move the string our helloworld is going to print out into the configuration file. The configration file follows .ini format, which means you have "sections" in square brackets followed by lines that set values for various names within the section. PEAK doesn't place many restrictions on what sections or names can appear in an ini file, so we can pretty much choose whatever section name and variable names we want, as long as they're valid Python identifiers.

We'll be very unoriginal and call our configuration section helloworld. The text of our message will be placed in the message configuration variable:

 
    #!/usr/bin/env peak runIni 
 
    [peak.running] 
 
    app = importString('helloworld:HellowWorld') 
 
    [helloworld] 
 
    message = "Hello, world!" 
 
At this point you could run helloworld again, and nothing would have changed. PEAK will have loaded the new configuration information we provided, but our program doesn't yet actually use that information.

To use the configuration information we need to obtain it via the PEAK configuration framework. At this point we are actually starting to use PEAK facilities directly in our Python code, so we'll need to get them out of the PEAK package. PEAK provides a simple way to access the vast majority of the PEAK facilities through its api module:

 
    from peak.api import * 
 
This will load into the local namespace a minimal set of names through which the PEAK frameworks can be accessed. It uses a lazy import mechanism, so ultimately your program will only end up loading into memory those PEAK facilities it actually uses.

In addition, we can no longer use a simple, dumb executable object as our app object. In order to access the configuration information, the app object must participate in the appropriate PEAK frameworks.

In this case, the framework for a simple command line command such as our helloworld is provided by the AbstractCommand class of the peak.running.commands module (lazily imported for us by peak.api):

 
    class HelloWorld(commands.AbstractCommand): 
 
An "abstract" class is a base class that cannot be used by itself. To be used, it must be subclassed and certain abstract methods overridden with "real" code in the concrete subclass.

In the case of AbstractCommand, there is only one method we need to override: the _run method, which will do the actual work of our application. So, without getting into the configurability yet, our complete helloworld.py file would now look like this:

    1     from peak.api import *
    2 
    3     class HelloWorld(commands.AbstractCommand):
    4 
    5         def _run(self):
    6             print "Hello, world!"
So, again, we can run our program at this point and get the same results as before. But we still haven't used the configuration information. To do that, we use the binding package.

The function of the peak.binding package that we are interested in here is the Obtain class. Obtain, coupled with the PropertyName class, allows us to pull values out of the configuration file:

 
    message = binding.Obtain(PropertyName('helloworld.message')) 
 

Obtain can find things using mechanisms other than PropertyName, but PropertyName is the one we are interested in here. PropertyName is a string subclass, and its constructor takes as its argument the fully qualified property name. By "fully qualified", I mean the name of the section the property is found in, followed by a dot, followed by the variable name. So the message property from our helloworld section becomes the PropertyName helloworld.message.

Bindings like Obtain are "descriptors", like the Python built-in type property. So, in order for a binding to be used, it must be placed in a class, and then used from an instance of the class.

So, when we access the message attribute in our _run() method, the Obtain binding will look up the configuration property for us. It does this by searching the context in which the instance is located. The peak.running.commands framework will provide us with with such a context automatically, when it creates an instance of our HelloWorld class. So here's our revised program:

    1     from peak.api import *
    2 
    3     class HelloWorld(commands.AbstractCommand):
    4 
    5         message = binding.Obtain(PropertyName('helloworld.message'))
    6 
    7         def _run(self):
    8             print self.message
With this version of helloworld.py we are finally using the configuration information provided in the helloworld file. To prove this, first run the helloworld command:
 
    PYTHONPATH=. ./helloworld 
 
Now copy helloworld to hellonewworld and edit it to look like this:
 
    #!/usr/bin/env peak runIni 
 
    [peak.running] 
 
    app = importString('helloworld:HelloWorld') 
 
    [helloworld] 
 
    message = "Hello, new world!" 
 
If you now run this new command:
 
    PYTHONPATH=. ./hellonewworld 
 
you'll be greated by our new message:
 
    Hello, new world! 
 
Although this is a very simple example, it should already be clear that PEAK provides an easy to use and powerful configuration mechanism. Simple use of PropertyName and values provided in config files barely even scratch the surface of what PEAK can do, but even if you used nothing else PEAK would still be useful just for making it easy to write configurable python shell scripts!

2 Command Arguments and Simple Database Access

2.1 Introduction

Now its time to go a tiny step beyond the triviality of the "Hello, world!" example. In this chapter I'll expand the example to handle saying hello to an arbitrary variety of things. Since we want to be flexible. we'll get the greeting message for each thing from a database table. (Well, really just a flat file, but that's so we don't get distracted with SQL and the like just yet.)

2.2 Command Arguments

To greet more than one thing, we'll need to be able to tell our command what to greet. Let's rename our command to hello, and give what we want to say hello to as the command argument, e.g.:

 
    hello world 
    hello fred 
    hello Klause 
 

If all we want to do is say hello in the same boring way every time, we could revise our helloworld.py file as follows:

    1     from peak.api import *
    2 
    3     class HelloWorld(commands.AbstractCommand):
    4 
    5         message = binding.Obtain(PropertyName('helloworld.message'))
    6 
    7         def _run(self):
    8             print self.message + self.argv[1]
The corresponding hello file would look like this:
 
    #!/usr/bin/env peak runIni 
 
    [peak.running] 
 
    app = importString('helloworld:HelloWorld') 
 
    [helloworld] 
 
    message = "Hello, " 
 
Now we'll get something like this:
 
    % export PYTHONPAT=. 
    % ./hello world 
    Hello, world 
    % ./hello fred 
    Hello, fred 
    % ./hello, Klause 
    Hello, Klause 
 

2.3 Models

When working with application data, PEAK uses the concept of a "domain model" for that data. A "domain model" describes the kinds of real-world objects you're working with, and their relationships to other kinds of objects. In PEAK-speak, the objects are called "Elements", and the relationships are called "Features". Features are implemented as object attributes, using custom descriptors.

To facilitate grabbing data from our database, we'll define a simple Element class:

    1     from peak.api import *
    2 
    3     class Message(model.Element):
    4 
    5         class forname(model.Attribute):
    6             referencedType = model.String
    7 
    8         class text(model.Attribute):
    9             referencedType = model.String
Here Message is our Element, the thing we are going to load from our database file. forname and text are attributes our Message objects will have once loaded. I think their purposes should be pretty obvious.

You probably noticed immediatly that we are defining classes inside classes here. The nested classes actually create attribute descriptors, similar to the Python built-in property type. However, instead of having to define functions and then wrap them into property object, we can simply subclass a predefined "Feature" type such as model.Attribute, and provide parameters such as referencedType, or define methods to control the feature's behavior.

Note that referencedType does not necessarily refer to the class of the objects or values that will be stored in the attribute. It can also reference an object like model.String, that simply provides metadata describing what values are acceptable for the attribute.

(For more extensive examples of using Model types, see the bulletins example in the examples directory of the PEAK CVS tree.)

In keeping with what seems to be a common practice for PEAK programmers, we'll store the above code in a file named model.py.

2.4 Data Managers

The domain model by itself is simply a schema, perhaps with some behavior. (For example, we might add a hello() method to our Message class, so that an instance of Message could actually deliver its message directly.)

But, a domain model by itself doesn't know anything about storage. (This is so that we can reuse the domain model with different kinds of storages.) To store and retrieve instances of our domain model classes, we need a Data Manager. Data Managers are responsible for loading and storing the data described by the domain model classes.

For the present example we're only interested in loading data from a "database table". So we'll subclass QueryDM, a peak.storage class that provides a read-only interface to a datastore:

 
    from peak.api import * 
 
    class MessageDM(storage.QueryDM): 
 
This class is another abstract class that has to be specialized for our intended use. Specifically, we have to add the code that does the actual reading and writing of data from the model attributes, to and from the external datastore.

To keep our example simple, we'll use a flat file as our external data store. In keeping with PEAK design principles, we won't hardcode the filename into our Data Manager, but will instead make it configurable:

 
        filename = binding.Obtain(PropertyName('helloworld.messagefile')) 
 
Obviously, we'll now need a different line in our hello configuration file::
 
    [helloworld] 
 
    messagefile = config.fileNearModule('helloworld', 'hello.list') 
 
Here we've used another PEAK service: config.fileNearModule will construct an appropriately qualified filename for the second argument based on the assumption that it is in the same directory as the module named by the first argument. So, messagefile will be the path to the hello.list file, located in the same directory as our helloworld.py file.

Since we're only using a QueryDM in this example, we only have to worry about reading data from the datastore, not writing it (That's a later example). To specify how to do this, we override the _load method of QueryDM. Our _load method needs to return a dictionary of names and values, which will get used through a __setstate__ call to load the data into our Message instances.

Now we need to whip up a data format for our messagefile. Let's have the thing we are saying hello to be first, and the actual message second, separated by a "|" character.

So we'll create a hello.list file like this:

 
    world  | Hello, world! 
    Fred   | Greatings, good sir. 
    Klause | Guten Aben, Herr Klause. 
 
(Forgive my feeble attempts at Deutsche.)

Because this is going to be a read-only file, we're going to cheat and load the file only once, the first time it's used. We'll use another peak.binding tool to accomplish this:

    1         def data(self):
    2             data = {}
    3             f = open(self.filename)
    4             for l in f:
    5                 parts = l.split('|')
    6                 for i in range(len(parts)): parts[i] = parts[i].strip()
    7                 data[parts[0]] = {'forname': parts[0], 'text': parts[1]}
    8             f.close()
    9             return data
   10 
   11         data = binding.Make(data)

binding.Make is similar to binding.Obtain, in that it's used inside a class body to create a property-like descriptor for the class' instances. It's different, in that it takes a function as its argument, rather than a configuration key. The function should take at least one parameter (self), and return the value to be used for an attribute. In this way, it's very similar to the property built-in, but with a key difference: a property's fget function is called every time it is used, but the result of a binding.Make function is cached and reused for subsequent accesses of the attribute.

So, here's what will happen. The first time an instance of our QueryDM subclass accesses its data attribute, the function above will be called, and the result stored in the instance's data attribute. It will then be immediately available for use, and won't be computed again for that instance unless the attribute is deleted.

(Of course, in the case of our current hello program, we'll only ever make one query on the database. If we were going to make a longer-running program, or allow the database to be modified, using this sort of caching might be a bad idea. However, this design decision affects only our data manager's implementation, and not the rest of the application. Our main, command-line application will not be affected, and neither will our Message class, if we decide to change how or where the messages are stored.)

Here's the complete contents of the last new file we need for our expanded hello application, the storage.py file. This also adds the _load method to the QueryDM:

    1     from peak.api import *
    2     from model import Message
    3 
    4     class MessageDM(storage.QueryDM):
    5 
    6         defaultClass = Message
    7         filename = binding.Obtain(PropertyName('helloworld.messagefile'))
    8 
    9         def data(self):
   10             data = {}
   11             f = open(self.filename)
   12             for l in f:
   13                 parts = l.split('|')
   14                 for i in range(len(parts)): parts[i] = parts[i].strip()
   15                 data[parts[0]] = {'forname': parts[0], 'text': parts[1]}
   16             f.close()
   17             return data
   18 
   19         data = binding.Make(data)
   20 
   21         def _load(self, oid, ob):
   22             return self.data[oid]
defaultClass specifies the class that will be used to instantiate objects retreieved from this Data Manager. In our case, that's Message from our model class. binding.Obtain you've met before, so its purpose here should be obvious.

A data manager is like a container for application objects. It's keyed by the notion of an oid: an "object ID" for objects of this kind. So, when we use a MessageDM instance, we'll retrieve objects from it like this:

 
    aMessage = myMessageDM[someName] 
 

When we do this, the MessageDM will return what's called a "ghost". It will be an instance of Message that contains no data, but knows its object ID, and knows that it's not yet loaded. As soon as we try to use the Message (by accessing any attributes or methods), it will "phone home" to the data manager it was retrieved from, asking for its data to be loaded.

At this point, the MessageDM._load() method is going to get called. It'll be given the object id that was used to access the object originally (the oid parameter), and the applicable "ghost" object (ob). The data that _load() returns will be used to fill in the ghost's instance dictionary so that it will become a "real" object, and the attribute access that triggered the _load call can finally be satisfied.

2.5 Putting it all Together

All that remains, then, is to use our new data manager from our main application program:

    1     from peak.api import *
    2     from model import Message
    3 
    4     class HelloWorld(commands.AbstractCommand):
    5 
    6         Messages = binding.Make(
    7             'storage:MessageDM',
    8             offerAs=[storage.DMFor(Message)]
    9             )
   10 
   11         def _run(self):
   12             storage.beginTransaction(self)
   13             print self.Messages[self.argv[1]].text
   14             storage.commitTransaction(self)
As you can see, our main program has stayed fairly simple, despite the additional complexity of using a database. (And in case you think "using a database" is an inflated way of refering to a flat file, observe that we can replace the simple flat file with access to something like an SQL database, simply by changing the _load method in our data manager.)

In our revised HelloWorld class, we see another use of binding.Make, this time taking an import string that specifies a class that should be instantiated. Previously, we used binding.Make with a function, but it also accepts classes, or strings that say where to import classes or functions from. (Indeed, it takes anything that implements or adapts to the binding.IRecipe interface, but that's more than you need to know right now.)

When used with a class (or an import string that names a class), binding.Make will call it once, the first time the named attribute is used. In this case, that means that it will automatically create a new MessageDM as the Messages attribute of the HelloWorld instance it's contained in. So, in effect we are declaring that each HelloWorld instance should have its own MessagesDM instance, stored in its Messages attribute.

Here we also use an additional keyword argument (offerAs) to binding.Make. offerAs is a list of "configuration keys" under which the created component will be "offered" to child components (via the configuration system). In this case, we're saying that any child components of a HelloWorld instance should use its Messages attribute, if they are looking for a data manager that provides storage services for Message instances.

The PEAK configuration system offers many kinds of "configuration keys" under which components or configuration properties can be found. We've previously worked with PropertyName, which is one kind of configuration key. And here we work with another, storage.DMFor(), that creates a configuration key denoting a data manager for a particular element type. PEAK does not limit you to using its predefined kinds of configuration keys, either. You can create your own for specialized purposes, by implementing the config.IConfigKey interface.

Once you "offer" an attribute as the source of a configuration key, it can then be referenced by other uses of binding.Obtain by child components. For example, if we had another class that needed to use a data manager for Message instances, we could add something like this to that class:

 
    Messages = binding.Obtain(storage.DMFor(Message)) 
 

This would use the configuration system to find an appropriate data manager. And, if an instance of this other class were contained within an instance of HelloWorld, it would use the HelloWorld object's Messages attribute to fill its own Messages attribute. (Note that the similarity in names has nothing to do with how it works; we could have called one of the attributes "foobar" and it would make no difference.)

Anyway, once we've got the data manager to be used for Message instances, we can look up any Message instance in it by using the instance's object id (in this case, its forname) as a key. In our case, that key is the string passed in as an argument to our command (self.argv[1], supplied to us by the peak script). As previously discussed, this gives us a Message instance, which we can then display by printing its text attribute.

Our last addition to HelloWorld is the use of beginTransaction and commitTransaction to enclose our data manipulation in a transaction. For our simple read-only application here, a transaction is of relatively little importance, but transaction management will matter a great deal when you use read/write databases.

Anyway, our program now works like this:

 
    % export PYTHONPATH=. 
    % ./hello world 
    > Hello, world! 
    % ./hello Fred 
    > Greetings, good sir. 
    % ./hello Klause 
    > Guten Aben, Herr Klause. 
 

For another example of using the binding and storage frameworks, check out PeakDatabaseApplications.


PythonPowered
EditText of this page (last modified 2003-12-06 11:22:13)
FindPage by browsing, title search , text search or an index
Or try one of these actions: AttachFile, DeletePage, LikePages, LocalSiteMap, SpellCheck