The PEAK Developers' Center   IntroToPeak/LessonThree UserPreferences
 
HelpContents Search Diffs Info Edit Subscribe XML Print View Up
Version as of 2003-12-09 22:06:16

Clear message


Up: IntroToPeak Previous: IntroToPeak/LessonTwo Next: IntroToPeak/LessonFour

Lesson Three: Subcommands and Storing Data

(Note: This lesson has not yet been edited by PJE)

We're stretching pretty hard here to find ways to use "Hello, world!" to demonstrate PEAK concepts. Just keep supressing your impulse to laugh at the triviality of the example, and keep focused on the concepts it's demonstrating.

At this point our hello program can greet anything whose name is recorded in the table. Suppose we have a new thing we want to greet? What we need is a way to update the database table to record a new greeting. Our hello program will now have two different functions we need it to perform: greeting, and recording new greetings. (Of course, we could also implement these as two separate commands, but then we wouldn't have an excuse to talk talk about AbstractInterpreter and demonstrate Bootstrap).

So in this lesson we'll expand the hello command to have subcommands:

 
    % ./hello for Jeff: Hi, guy! 
    % ./hello to Jeff 
    Hi, guy! 
 
This will require revising our storage implementation to allow writing to our database. We'll stick to using a file for now, to keep the distraction of SQL at bay for a little while longer.

Contents

  1. Lesson Three: Subcommands and Storing Data
    1. AbstractInterpreter and Bootstrap
    2. peak.running.shortcuts
    3. Storing a New Message: the "for" Subcommand
    4. Storing a New Message: Modifying the Data Manager
    5. Oh no, a bug!

AbstractInterpreter and Bootstrap

Another abstract class provided by PEAK is AbstractInterpreter. This class represents something PEAK can run that will execute subcommands based on the first argument word. PEAK also provides an subclass of AbstractInterpreter called Bootstrap, that looks up a URL or command shortcut and runs it.

If you think that sounds a lot like what the peak script does, you're quite correct. If you take a look at the actual peak script, it looks something like:

    1 #!/usr/bin/env python2.2
    2 
    3 from peak.running import commands
    4 commands.runMain( commands.Bootstrap )

Which means that we've actually been using Bootstrap all along, to run our programs. Since we'd like to be able to use commands like hello to and hello for in the same way that we can use peak help or peak runIni, we'll make our new main program a subclass of commands.Bootstrap:

    1 from peak.api import *
    2 from hello_model import Message
    3 
    4 
    5 class HelloWorld(commands.Bootstrap):
    6 
    7     usage = """
    8 Usage: hello command arguments
    9 
   10 Available commands:
   11 
   12     for -- sets a greeting
   13     to  -- displays a greeting
   14 """
   15 
   16     Messages = bindings.Make(
   17         'hello_storage.MessageDM',  offerAs=[storage.DMFor(Message)]
   18     )
   19 
   20 
   21 class toCmd(commands.AbstractCommand):
   22 
   23     usage = """
   24 Usage: hello to <name>
   25 
   26 Displays the greeting for "name".
   27 """
   28 
   29     Messages = binding.Obtain(storage.DMFor(Message))
   30 
   31     def _run(self):
   32         storage.beginTransaction(self)
   33         print >>self.stdout, self.Messages[self.argv[1]].text
   34         storage.commitTransaction(self)
So we've got a new HelloWorld main program, this time a subclass of commands.Bootstrap. HelloWorld is still the holder for the MessageDM binding. The Bootstrap class will automatically make the subcommands into children of our HelloWorld component, so the subcommands will be able to Obtain the DM from their context.

The only other thing it's got is a usage class variable. To see how this is used, try typing ./hello at your command prompt:

 
% ./hello 
 
Usage: hello command arguments 
 
Available commands: 
 
    for -- sets a greeting 
    to  -- displays a greeting 
 
 
./hello: missing argument(s) 
 
As you can see, PEAK is taking care of a lot of the routine tasks associated with writing a script!

Our original AbstractCommand is still there, but now we've named it toCmd. And, since it a different class from HelloWorld where we defined the binding to our MessageDM, it needs to Obtain that binding. Remember, it is thereby getting access to the same MessageDM instance as the one in the associated HelloWorld instance. So, by using Make(...,offerAs=[something]) in a parent component, and Obtain(something) in child components, a parent component can share one instance of a service with any child component that needs it.

peak.running.shortcuts

Next, we need to hook up the toCmd class so it can be invoked as a subcommand. How can we do that? Remember when we looked at the help for the peak script? The first paragraph said:

  
The 'peak' script bootstraps and runs a specified command object or command  
class.  The NAME_OR_URL argument may be a shortcut name defined in the  
'peak.running.shortcuts' property namespace, or a URL of a type  
supported by 'peak.naming'. 
 

Hm. If we look at the peak.ini file, we see a section called [peak.running.shortcuts], containing a bunch of properties called runIni, help, and many other commands.

Does this mean that if we add a similar section to our hello file, we can create subcommands of our own? Let's try adding this new section to hello:

 
[peak.running.shortcuts] 
to = importString('helloworld.toCmd') 
 

and now try it:

 
    % ./hello to Fred 
    Greetings, good sir. 
 

Excellent! By the way, you may have noticed that when we turned our command into a subcommand, we did not need to change our argv index number. The argument array stored in self.argv of the subcommand has the subcommand name in argv[0], and the rest of the arguments starting in argv[1]. That's because AbstractInterpreter classes like Bootstrap automatically shift the arguments over for us when they create the subcommand object.

Also by the way, we should mention that it wasn't strictly necessary to edit the configuration file to do what we just did. We also could have defined a binding in our HelloWorld class to "offer" the right configuration value, like this:

 
    __toCmd = binding.Obtain( 
        'import:helloworld.toCmd',  
        offerAs=['peak.running.shortcuts.to'] 
    ) 
 

But now that you've seen how, you can also see why we didn't do it. It's rather ugly to do this sort of configuration in code, compared to using an .ini file. But it's nice to know you can do it if you need to.

Of course, the configuration file is also more flexible: notice, for example, that we could make multiple configuration files for the same code, each file specifying a different set of subcommands, perhaps for different users of the app. You could almost say that PEAK's motto is, "code reuse, through flexibility".

Now for something completely different. Let's try this:

 
    % ./hello to 
 
Given how ./hello magically generated a usage string, you might think this would do so as well. After all, we provided one in the code above, right? Well, an AbstractCommand doesn't automacially display a usage when no arguments are supplied because, after all, no arguments might be required. It will automatically display the usage if we raise a commands.InvocationError, in our _run method, though:
    def _run(self): 
        if len(self.argv)<2: 
            raise commands.InvocationError("Missing name") 
        storage.beginTransaction(self) 
        print >>self.stdout, self.Messages[self.argv[1]].text 
        storage.commitTransaction(self) 

Now we'll get:

 
%s ./hello to 
 
Usage: hello to <name> 
 
Displays the greeting for "name". 
 
to: Missing name 
 

There's just one problem left with the hello command. Try running hello runIni, and see what we get:

 
% ./hello runIni 
 
Usage: peak runIni CONFIG_FILE arguments... 
 
CONFIG_FILE should be a file in the format used by 'peak.ini'.  (Note that 
it does not have to be named with an '.ini' extension.)  The file should 
define a 'running.IExecutable' for the value of its 'peak.running.app' 
property.  The specified 'IExecutable' will then be run with the remaining 
command-line arguments. 
 
 
runIni: missing argument(s) 
 

Whoops! Just because our configuration file contains its own [peak.running.commands] section, doesn't mean that the settings in peak.ini don't apply. We need to do something about this, so that hello doesn't reuse all the peak subcommands.

Looking at peak.ini, we sometimes see that properties sometimes end with a *. What happens if we define a * rule in the shortcuts section,?

 
[peak.running.shortcuts] 
*  = commands.NoSuchSubcommand 
to = importString('helloworld.toCmd') 
 

Let's try it now:

 
% ./hello runIni 
 
Usage: hello command arguments 
Available commands: 
 
    for -- sets a greeting 
    to  -- displays a greeting 
 
 
runIni: No such subcommand 'runIni' 
 

Good. To recap, we used commands.NoSuchSubcommand, which raises an InvocationError for us, and we used a * rule to define a default value for properties whose names are within a particular "property namespace". That is, any name we look up in peak.running.shortcuts from our configuration file, that isn't explicitly defined there or in our app, will return the commands.NoSuchSubcommand class. That's just what we want for now.

Actually... there is still one more problem. commands.Bootstrap also accepts URLs on the command line by default. commands.Bootstrap provides a way to turn that behvior off, though. We just need to add a flag variable to our HelloWorld class:

    1 class HelloWorld(commands.Bootstrap):
    2 
    3     acceptURLs = False

With these changes, our Bootstrap derivative will now do the right thing. Let's move on to the for command now.

Storing a New Message: the "for" Subcommand

Now, we know we're going to have to rewrite our hello_storage.py to allow us to write to the databse, but let's start this part of the task by writing the subcommand first. As you'll quickly see, any consideration of how we implement the saving of the data is completely independent of how we go about initiating the save in the application program.

So, we need another rule in our hello configuration file:

 
[peak.running.shortcuts] 
*  = commands.NoSuchSubcommand 
to = importString('helloworld.toCmd') 
for = importString('helloworld.forCmd') 
 
and another AbstractCommand subclass in helloworld.py

 
class forCmd(commands.AbstractCommand): 
 
    usage = """ 
Usage: hello for <name>: <greeting> 
 
Stores "greeting" as the greeting message for "name". 
""" 
 
    Messages = binding.Obtain(storage.DMFor(Message)) 
 
    def _run(self): 
        if len(self.argv)<2:  
            raise commands.InvocationError("Missing arguments") 
        parts = ' '.join(self.argv[1:]).split(':',1) 
        if len(parts)!=2: 
            raise commands.InvocationError("Bad argument format") 
        forname, message = parts 
        storage.beginTransaction(self) 
        newmsg = self.Messages.newItem() 
        newmsg.forname = forname.strip() 
        newmsg.text = message.strip() 
        storage.commitTransaction(self) 
 
To put a new object in our database, we ask the Data Manager for a new "blank" object. (Actually, it can have a preloaded default state, but we'll ignore that for now). Then we modify it just like we would any other writable object we got from the Data Manager, and the transaction machinery takes care of getting the data written to the backing store at transaction commit time.

At this point the for subcommand of our hello command is runable:

 
% ./hello for 
 
Usage: hello for <name>: <greeting> 
 
Stores "greeting" as the greeting message for "name". 
 
for: Missing arguments 
 
% ./hello for foobar 
 
Usage: hello for <name>: <greeting> 
 
Stores "greeting" as the greeting message for "name". 
 
for: Bad argument format 
 
% ./hello for Jeff: Hi, guy! 
Traceback (most recent call last): 
  File "/usr/local/bin/peak", line 4, in ? 
    commands.runMain( commands.Bootstrap ) 
  File "/usr/local/lib/python2.3/site-packages/peak/running/commands.py", line 70, in runMain 
    result = factory().run() 
  File "/usr/local/lib/python2.3/site-packages/peak/running/commands.py", line 211, in run 
    return self._run() or 0 
  File "/var/home/rdmurray/proj/peak/helloworld/07writabledb/helloworld.py", line 53, in _run 
    newmsg = self.Messages.newItem() 
AttributeError: 'MessageDM' object has no attribute 'newItem' 
 
Ah, yes. As you'll recall, we used a read-only Data Manager base class when we developed our database. So we can't store anything until we fix that.

Storing a New Message: Modifying the Data Manager

OK, it's time to do some serious surgery on our Data Manager. As I said before, I'm still going to keep us away from SQL for the moment. But if you'll recall, I mentioned how important transactions are when writing to a database. Writing to normal file system files is not transaction oriented, so what can we do?

PEAK provides a useful utility class that partially solves this problem: EditableFile. EditableFile is an object that represents a file and participates in the PEAK transaction machinery.

Here's what the EditableFile docstring has to say:

    """File whose text can be manipulated, transactionally 
 
    Example:: 
 
        myfile = EditableFile(self, filename="something") 
        print myfile.text   # prints current contents of file 
 
        # Edit the file 
        storage.beginTransaction(self) 
        myfile.text = myfile.text.replace('foo','bar') 
        storage.commitTransaction(self) 
 
    Values assigned to 'text' will be converted to strings.  Setting 'text' 
    to an empty string truncates the file; deleting 'text' (i.e. 
    'del myfile.text') deletes the file.  'text' will be 'None' whenever the 
    file is nonexistent, but do not set it to 'None' unless you want to replace 
    the file's contents with the string '"None"'! 
 
    By default, files are read and written in "text" mode; be sure to supply 
    a 'fileType="b"' keyword argument if you are editing a binary file.  Note 
    that under Python 2.3 you can also specify 'fileType="U"' to use "universal 
    newline" mode. 
 
    'EditableFile' subclasses 'TxnFile', but does not use 'autocommit' mode, 
    because it wants to support "safe" alterations to existing files. 
This looks pretty straightfoward to use, especially since we can assume, since we are writing in a Data Manager, that we will be inside a transaction and don't have to worry about that aspect here.

OK, now that we've got a way to transactionally update a file, we need to exchange our QueryDM base class to a base class that supports updating the database. That would be EntityDM.

EntityDM requires two additional methods to be defined by the concrete class: _new, and _save. _new is called when a new object is added to the DM, and needs to store the data for that object in the external database. _save is called when an object's state is changed, and a transaction boundry has been reached where that state needs to be synchronized with the external database.

At this point our original scheme of reading and parsing the file and storing the results the data attribute is going to break down. With our current user interface it would actually work just fine. The file would get reparsed on each command invocation. But suppose we later rewrite the interface so that multiple commands can be issued in the same run of the application? If we keep our pre-parsed data attribute, a message updated by a for command wouldn't be seen by subsequent to commands.

So, we're going to have to use the EditableFile instance as our reference for each lookup from the database. Our new hello_storage.py will now look like this:

    1 from peak.api import *
    2 from peak.storage.files import EditableFile
    3 from hello_model import Message
    4 import re
    5 
    6 class MessageDM(storage.EntityDM):
    7 
    8     defaultClass = Message
    9     fn = binding.Obtain(PropertyName('helloworld.messagefile'))
   10 
   11     def file(self):
   12         return EditableFile(filename=self.fn)
   13 
   14     file = binding.Make(file)
   15 
   16     def __makeRe(self, oid):
   17         return re.compile(r"^%s[|](.*)$" % oid, re.M)
   18 
   19     def __findMatch(self, oid, text):
   20         return self.__makeRe(oid).search(text)
   21 
   22     def _load(self, oid, ob):
   23         m = self.__findMatch(oid, self.file.text)
   24         return {'forname': oid, 'text': m.group(1)}
   25 
   26     def _new(self, ob):
   27         self.file.text += "%s|%s\n" % (ob.forname, ob.text)
   28 
   29     def _save(self, ob):
   30         self.file.text = self.__makeRe(ob.forname).sub(
   31             "%s|%s" % (ob.forname, ob.text), self.file.text)
Well, that actually got a little simpler, didn't it? But I notice that in my attempts to (temporarily) stay away from the complexities of SQL, I seem to have wandered into the complexities of regular expressions.

Let's get that out of the way first. The not-too-gnarly little regular expression I use is designed to do two things: it matches any line (that's the re.M bit) that starts with the oid followed by a vertical bar, and when used for a search it returns a match object that has a group containing everything after the vertical bar to the next newline.

This regular expression is only going to work if there's no space between the forname and the vertical bar, but since we're now updating the database with our program as well as reading it, we can insure that. If you are following along with running code, though, you might want to edit the old hello.list to remove the spaces.

There are a few things to notice about our revised DM.

We're still getting the filename from that same configuration variable. Now, however, we are turning that into an EditableFile. Again we use binding.Make to create a descriptor that will return a real value (and cache it) when the class attribute is actually accessed.

Note that without binding.Make here we'd be stuck, since if we tried to do something like

 
file = EditableFile(filename=fn) 
 
our application would blow up when we tried to access the EditableFile, because it would have hold of the descriptor instance instead of being able to access the filename through the descriptor. (To PJE: I suspect I'm explaining this badly, because I only half understand it; hopefully you can clarify it)

In the _load method the match group in the regex means the parsing out of the message is already done for us, and all we have to do is return the data.

The _new method simply appends an appropriate record to the file.

The _save method uses re's substitution method to generate a new version of the string representing the file with the body of the line matching the forname replaced by the new value.

With this code in place our for method is working:

 
% ./hello for Jeff: Hi, guy! 
% ./hello to Jeff 
Hi, guy! 
 

Oh no, a bug!

At this point certain readers are getting antsy because there's a bug in the for method we created up above. Assuming you've got all four of the entries in your database we've been using for examples, what happens if you do this:

 
% ./hello for Jeff: Long time, no see 
 
Obviously, the intent is to replace the current message for Jeff with a new one. However, our for code assumed the forname passed to it was a new name. Currently, the code will write a new entry to the database, which because of our storage implementation will append a new "Jeff" record to the end of the file. So techncially we've got a bug in our database implementation, too.

Let's fix that problem first.

If a regex doesn't match, re will return None. So in our _new method we can check to see if the new forname is already in the database or not, and raise an error if it is:

    1     def _new(self, ob):
    2         if self.__findMatch(ob.forname, self.file.text):
    3             raise KeyError, "%s is already in the database" % ob.forname
    4         self.file.text += "%s|%s\n" % (ob.forname, ob.text)

Now at least we'll get an error if we use our buggy for subcommand:

 
% ./hello for Jeff: Hello, dude. 
  [...first part of traceback elided...] 
  File "/usr/local/lib/python2.3/site-packages/peak/storage/data_managers.py", line 432, in flush 
    oid = ob._p_oid = self._new(ob) 
  File "/var/home/rdmurray/proj/peak/helloworld/07writabledb/hello_storage.py", line 27, in _new 
    raise KeyError, "%s is already in the database" % ob.forname 
KeyError: 'Jeff is already in the database' 
 

Fixing the for command feels a little less "natural". In fact, this is a limitation of the current DM framework that is slated to be fixed in alpha 4.

For now one way to handle this is to implement a __contains__ for the DM;

    1     def __contains__(self, oid):
    2         return self.__findMatch(oid, self.file.text) is not None
Armed with this, we can rewrite our for method as follows:
    1     def _run(self):
    2         if len(self.argv)<2: raise commands.InvocationError("Missing arguments")
    3         parts = ' '.join(self.argv[1:]).split(':')
    4         if len(parts)!=2: raise commands.InvocationError("Bad argument format")
    5         forname = parts[0].strip(); message = parts[1].strip()
    6         storage.beginTransaction(self)
    7         if forname in self.Messages:
    8             msg = self.Messages[forname]
    9         else:
   10             msg = self.Messages.newItem()
   11             msg.forname = forname
   12         msg.text = message
   13         storage.commitTransaction(self)

With this change, updating the database works:

 
% ./hello for Jeff: Hey, Dude! 
% ./hello to Jeff 
Hey, Dude! 
 

Up: IntroToPeak Previous: IntroToPeak/LessonTwo Next: IntroToPeak/LessonFour


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