IntroToPeak/LessonThree |
UserPreferences |
The PEAK Developers' Center | FrontPage | RecentChanges | TitleIndex | WordIndex | SiteNavigation | HelpContents |
Up: IntroToPeak Previous: IntroToPeak/LessonTwo Next: IntroToPeak/LessonFour
(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
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:
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.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)
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.
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 toGiven 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.
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.
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:
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.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)
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!
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 seeObviously, 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;
Armed with this, we can rewrite our for method as follows:1 def __contains__(self, oid): 2 return self.__findMatch(oid, self.file.text) is not None
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