The PEAK Developers' Center   Diff for "IntroToPeak/LessonThree" UserPreferences
 
HelpContents Search Diffs Info Edit Subscribe XML Print View Up
Ignore changes in the amount of whitespace

Differences between version dated 2003-12-09 22:06:16 and 2005-02-01 11:15:34 (spanning 22 versions)

Deletions are marked like this.
Additions are marked like this.

 
= 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

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
two separate commands, but then we wouldn't have an excuse to
talk about `AbstractInterpreter` and demonstrate `Bootstrap`).
 
So in this lesson we'll expand the `hello` command to have

`commands.Bootstrap`: {{{
#!python
from peak.api import *
from hello_model import Message
from helloworld.model import Message
 
 
class HelloWorld(commands.Bootstrap):

    to -- displays a greeting
"""
 
    Messages = bindings.Make(
        'hello_storage.MessageDM', offerAs=[storage.DMFor(Message)]
    Messages = binding.Make(
        'helloworld.storage.MessageDM', offerAs=[storage.DMFor(Message)]
    )
 
 

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 subcommands will be able to `Obtain` the DM from their context,
as discussed in the last chapter.
 
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: {{{
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
 

./hello: missing argument(s)
 
}}} As you can see, PEAK is taking care of a lot of the routine
tasks associated with writing a script!
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
''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

 
}}}
 
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.
Hmm. So, if something defined in a particular "property namespace"
affects the way the peak command behaves, that must mean that the
peak command has some place to get those properties, which means
it probably has an ini file. Sure enough, a little poking around the
peak directories will reveal a `peak.ini` file. In that file we
can find 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')
to = importString('helloworld.commands.toCmd')
 
}}}
 

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: {{{
 
#!python
    __toCmd = binding.Obtain(
        'import:helloworld.toCmd',
        'import:helloworld.commands.toCmd',
        offerAs=['peak.running.shortcuts.to']
    )
 
}}}
 
But now that you've seen how, you can also see why we didn't do it.

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
after all, no arguments might be required. It ''will'' automatically
display the usage if we raise a `commands.InvocationError`, in our
`_run` method, though:
{{{
#!python
    def _run(self):
        if len(self.argv)<2:
            raise commands.InvocationError("Missing name")

}}}
 
Whoops! Just because our configuration file contains its own
`[peak.running.commands]` section, doesn't mean that the settings in
`[peak.running.shortcuts]` 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.
 

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

 
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: {{{
provides a way to turn that behavior off, though. We just need to
override a flag in our `HelloWorld` class: {{{
#!python
class HelloWorld(commands.Bootstrap):
 
    acceptURLs = False
 
    # rest of HelloWorld class goes here...
}}}
 
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
Now, we know we're going to have to rewrite our `storage.py` to
allow us to write to the database, 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
virtually independent of how we go about initiating the save
in the application program.
 
So, we need another

 
[peak.running.shortcuts]
* = commands.NoSuchSubcommand
to = importString('helloworld.toCmd')
for = importString('helloworld.forCmd')
to = importString('helloworld.commands.toCmd')
for = importString('helloworld.commands.forCmd')
 
}}} and another `AbstractCommand` subclass in `helloworld.py`
}}} and another `AbstractCommand` subclass in `commands.py`
 
{{{
 
#!python
class forCmd(commands.AbstractCommand):
 
    usage = """

    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)
 
        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.
a new "empty" object, using `newItem()`. (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: {{{

 
== 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`.
OK, it's time to do some serious surgery on our Data Manager.
First, we need to exchange our `QueryDM` base class for a base
class that supports updating the database. That would be
`storage.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 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: {{{
Let's write the new `storage.py`: {{{
#!python
from peak.api import *
from peak.storage.files import EditableFile
from hello_model import Message
import re
from helloworld.model import Message
 
class MessageDM(storage.EntityDM):
 
    defaultClass = Message
    fn = binding.Obtain(PropertyName('helloworld.messagefile'))
    filename = binding.Obtain(PropertyName('helloworld.messagefile'))
 
    def file(self):
        return EditableFile(filename=self.fn)
    def data(self):
        data = {}
        file = open(self.filename)
        for line in file:
            fields = [field.strip() for field in line.split('|',1)]
            forname, text = fields
            data[forname] = {'forname': forname, 'text': text}
        file.close()
        return data
 
    file = binding.Make(file)
 
    def __makeRe(self, oid):
        return re.compile(r"^%s[|](.*)$" % oid, re.M)
 
    def __findMatch(self, oid, text):
        return self.__makeRe(oid).search(text)
    data = binding.Make(data)
 
    def _load(self, oid, ob):
        m = self.__findMatch(oid, self.file.text)
        return {'forname': oid, 'text': m.group(1)}
        return self.data[oid]
 
    def _new(self, ob):
        self.file.text += "%s|%s\n" % (ob.forname, ob.text)
        self._save(ob)
        return ob.forname
 
    def _save(self, ob):
        self.file.text = self.__makeRe(ob.forname).sub(
            "%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.
    def _save(self,ob):
        self.data[ob.forname] = {'forname':ob.forname, 'text':ob.text}
}}}
 
There are a few things to notice about our revised DM.
That was easy. The `_new()` method is responsible for both saving state and returning the object ID of the new object. This is because `_new` is responsible for assigning object IDs. In this case, we simply return `ob.forname`, since that's what we're using as an object ID, after calling `self._save(ob)`. Let's run the script, and try it out: {{{
 
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.
% ./hello for Fred: Hi, guy!
% ./hello to Fred
Greetings, good sir.
 
}}}
 
Note that without binding.Make here we'd be stuck, since if
we tried to do something like {{{
Oops. All we did was update our in-memory `data` dictionary. We didn't save it to disk, so the change didn't stay in place for long. How can we fix that?
 
file = EditableFile(filename=fn)
If we look at the `storage.IWritableDM` interface (see `peak help storage.IWritableDM`), we'll see that it includes a `flush()` method. `flush()` is called as part of the transaction commit process, and the default implementation of this method in `EntityDM` is what calls our `_save()` and `_new()` methods for the appropriate objects. If we define our own version of `flush()` that first calls the standard `flush()` and then writes our `data` array to disk, we'll be all set: {{{
#!python
    def flush(self,ob=None):
        super(MessageDM,self).flush(ob)
        file = open(self.filename,'w')
        for forname, data in self.data.items():
            print >>file, "%s|%s" % (forname,data['text'])
        file.close()
}}}
 
But wait. What if there's an error while writing the file? What is going to happen to the original file? Since we're opening the existing file for output, we'll have already erased our original data. That's not good.
 
We need a mechanism for writing files that can roll back or commit, just like the transaction as a whole. PEAK has a `peak.storage.files` module with two classes we can use for this: `TxnFile` and `EditableFile`. Because we're dealing with such a small file, and can load it all in memory at once, we'll use `EditableFile`, which offers a more convenient interface for such files. Let's take a look at the part of the output from `peak help peak.storage.files` that covers `EditableFile`: {{{
    class EditableFile(TxnFile)
     | 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.
}}}
 
Yep, that looks like what we need. We should be able to easily load and save
our data by reading or writing to the `EditableFile` object's `text` attribute,
especially since we will already be inside a transaction whenever we use
the data manager.
 
}}} 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)
Okay, so let's fix up `storage.py` to use `EditableFile`: {{{
#!python
from peak.api import *
from peak.storage.files import EditableFile
from helloworld.model import Message
 
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.
class MessageDM(storage.EntityDM):
 
The `_new` method simply appends an appropriate record to the file.
    defaultClass = Message
    filename = binding.Obtain(PropertyName('helloworld.messagefile'))
 
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.
    file = binding.Make(
        lambda self: EditableFile(filename=self.filename)
    )
 
With this code in place our `for` method is working: {{{
    def data(self):
        data = {}
        for line in self.file.text.strip().split('\n'):
            fields = [field.strip() for field in line.split('|',1)]
            forname, text = fields
            data[forname] = {'forname': forname, 'text': text}
        return data
 
% ./hello for Jeff: Hi, guy!
% ./hello to Jeff
Hi, guy!
    data = binding.Make(data)
 
    def _load(self, oid, ob):
        return self.data[oid]
 
    def _new(self, ob):
        self._save(ob)
        return ob.forname
 
    def _save(ob):
        self.data[ob.forname] = {'forname':ob.forname, 'text':ob.text}
 
    def flush(self,ob=None):
        super(MessageDM,self).flush(ob)
        self.file.text = ''.join(
            ["%s|%s\n" % (forname,data['text'])
                for forname, data in self.data.items()
            ]
        )
}}}
 
We hardly changed a thing. Instead of opening `self.filename` to read and write the data, now we simply split or join `self.file.text`. The `EditableFile` will automatically handle writing the new data to a different filename, then renaming it and replacing the old file. It'll also automatically discard the new file if the transaction is aborted for any reason.
 
== Oh no, a bug! ==
Speaking of aborting, there's actually still a bug in this DM. If a transaction is aborted, the DM may or may not have called `_new()`, `_save()` and/or `flush()`, one or more times already. The `EditableFile` will take care of resetting itself if a transaction is aborted, but our `data` dictionary could wind up out-of-sync with the file.
 
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: {{{
An easy way to do this would be to override the `abortTransaction()` method, similar to what we did for `flush()`, and delete the `data` dictionary if the transaction is aborted: {{{
#!python
    def _new(self, ob):
        if self.__findMatch(ob.forname, self.file.text):
            raise KeyError, "%s is already in the database" % ob.forname
        self.file.text += "%s|%s\n" % (ob.forname, ob.text)
    def abortTransaction(self, ob):
        self._delBinding("data")
        super(MessageDM,self).abortTransaction(ob)
}}}
 
Now at least we'll get an error if we use our buggy
`for` subcommand: {{{
Now, if the transaction is aborted, the `data` attribute gets deleted, and the next time we try to use it, our `binding.Make()` wrapper will re-run the function that creates the dictionary from the `EditableFile`. `EditableFile` does something similar to this, so when we access its `text` again, it will have reverted to whatever was last stored on disk, not what we changed it to.
 
% ./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'
There are a few other 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. We used a lambda expression here instead of a function,
as this is more readable when there's only a single expression
being executed.
 
Anyway, with these changes in place, our `for` method should
now be working: {{{
 
% ./hello for Jeff: Hi, guy!
% ./hello to Jeff
Hi, guy!
 
}}}
 
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; {{{
== Questioning Existence, and Tuning Performance ==
 
At this point certain readers may be getting antsy because there's a
flaw in the `forCmd` implementation. As we wrote it, the `for` command
assumes that it's always creating a ''new'' `Message`, even
though the `forname` may already exist in our primitive "database".
 
For our current example, this doesn't actually cause any problems:
because of the way we're updating the "database", it doesn't matter
if the item is new or an update. But, we don't want to rely on
this implementation quirk, and when we move to an SQL database
later on, it will matter quite a bit whether we're adding or
updating.
 
To fix this, we need to change our `for` command to check whether
the name exists, and then either update the existing `Message`
object, or create a new one, as appropriate. In order to
do that, we need to be able to ask the DM whether or not a given
key exists. Since we're using the `forname` as the object id, we
can handily provide a way to do it by adding a `__contains__`
method to the DM:
{{{
#!python
    def __contains__(self, oid):
        return self.__findMatch(oid, self.file.text) is not None
    def __contains__(self,oid):
        return oid in self.data
}}}
 
}}} Armed with this, we can rewrite our `for` method as follows: {{{
Now we can update our `forCmd._run()` method in `commands.py`: {{{
#!python
    def _run(self):
        if len(self.argv)<2: raise commands.InvocationError("Missing arguments")
        parts = ' '.join(self.argv[1:]).split(':')
        if len(parts)!=2: raise commands.InvocationError("Bad argument format")
        forname = parts[0].strip(); message = parts[1].strip()
 
        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 = [part.strip() for part in parts]
 
        storage.beginTransaction(self)
 
        if forname in self.Messages:
            msg = self.Messages[forname]
        else:
            msg = self.Messages.newItem()
            msg.forname = forname
 
        msg.text = message
        storage.commitTransaction(self)
 
}}}
 
With this change, updating the database works: {{{
With this change, updating the database should still work: {{{
 
% ./hello for Jeff: Hey, Dude!
% ./hello to Jeff

 
}}}
 
Sharp-eyed readers will notice that the `__contains__` method we wrote does
''double'' the normal work for retrieving an item, because it actually "loads"
data, by accessing the "database". Then, if the item exists in the database,
the `_load()` method will access the database again. For our in-memory
database, this is no big deal, but it will be more important when start
using SQL. Let's change our approach. We'll replace the `__contains__`
method with a `get` method: {{{
#!python
    def get(self,oid,default=None):
 
        if oid in self.data:
            return self.preloadState(oid, self.data[oid])
 
        return default
}}}
 
This method will either retrieve the object, or return the `default`,
which is the standard python signature for `get`. To support only
retrieving the object once (as well as various other situations),
DM's have a `preloadState(oid,state)` method. This method creates
a pre-loaded object, using `state`, ''instead of'' calling `_load`
to get the state. (Remember, what we have stored in `data` is a
dictionary containing the values for the various object attributes,
which is the state from the DM's point of view).
 
So, our `get()` method can load the state from our "database", and
then preload it into the object it returns.
 
There is actually still a minor inefficiency here: we're always
checking the "database", even if the object we want is already loaded
into memory. We can make this slightly more efficient by changing it
to: {{{
#!python
    def get(self,oid,default=None):
 
        if oid in self.cache:
            return self.cache[oid]
 
        elif oid in self.data:
            return self.preloadState(oid, self.data[oid])
 
        return default
}}}
DM's have a `cache` attribute that holds onto currently loaded objects,
so that multiple requests for a given object ID will always return the
same object. So, by checking it here first, we can avoid doing the
lookup in `self.data` if the requested object is already loaded.
 
These minor changes are of little or no consequence to our current
app, but will have more impact when we move to using SQL, as every
`self.data` lookup is going to end up as an SQL query.
 
Let's finish out our refactoring by updating `forCmd` to use our new
`get()` method: {{{
#!python
    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 = [part.strip() for part in parts]
 
        storage.beginTransaction(self)
 
        msg = self.Messages.get(forname)
 
        if msg is None:
            msg = self.Messages.newItem()
            msg.forname = forname
 
        msg.text = message
 
        storage.commitTransaction(self)
}}}
 
There. That even simplifies the logic a little. Note, by the way,
that we do ''not'' pass `Messages.newItem()` as the `default`
argument to `get()`, because that would do two wrong things: 1)
it'd create a new object that would be added to the database at
transaction commit, even if we didn't need it, and 2) it wouldn't
set `forname` on the new message. We could work around problem #2,
but not problem #1. Using the `newItem()` method of a DM always
creates an object that the DM will attempt to save when the transaction
commits, even if you don't keep the object around that long. So:
never call `newItem()` unless you want the object to be added to
the database. (Note: it's ''possible'' to write a DM that doesn't
behave this way, and only saves an object if it's referenced from
other objects or some kind of "root" object. We're just not going
to show you how in this tutorial!)
 
Anyway, that about wraps it up for creating a practical `EntityDM` subclass.
 
== Points to Remember ==
 
Here's the recap for what we've learned in this lesson (once again, it's quite a lot!):
 
 * Subcommands
 
  * The `peak` script is based on `commands.Bootstrap`, a `commands.AbstractInterpreter` subclass that runs a "subcommand" specified as its first argument.
 
  * `commands.Bootstrap` looks up non-URL commands in the `peak.running.shortcuts` property namespace
 
  * Raising `commands.InvocationError` in a command's `_run()` method causes the command's `usage` message to be displayed, along with the text of the `InvocationError` instance.
 
 * Configuration
 
  * Application configuration files load after `peak.ini`, and only explicit settings in an application config file override those in `peak.ini`
 
  * Using an asterisk (`*`) as the last part of a property name, defines a rule for all undefined properties in that property namespace. So, defining a rule for `peak.running.shortcuts.*` provides a value for any undefined property whose name begins with `"peak.running.shortcuts."` (Important: note the trailing dot).
 
 * Writable DM Basics
 
  * The `newItem()` method of a writable data manager (`storage.IWritableDM`) returns a new, "empty" object which will be saved when the transaction is committed.
 
  * The `storage.QueryDM` class does not support adding or modifying items, but `storage.EntityDM` does
 
  * The `_new()` method of an `EntityDM` must add the new object to the underlying database, and return the object ID to be used for the object
 
  * The object ID returned by `_new()` can be a newly-generated ID, or it can be a primary key field.
 
  * The `_save()` method of an `EntityDM` must update an existing object in the underlying database
 
 * Extending `EntityDM`
 
  * The `flush()` method of a writable data manager is called to write in-memory state to external databases. It can be overridden so that `_new()` and `_save()` can batch the data to be written, and then `flush()` can perform the batch in bulk.
 
  * If you are already accessing an underlying database (e.g. to check if an object exists, or when performing a mass query), you can use the `preloadState()` method to retrieve an object from the DM. The DM will not call its `_load()` method, but instead use the state that you supply to the `preloadState()` call.
 
  * DM's have a `cache` attribute, that caches "ghosts" and active objects used in the current transaction, to ensure that multiple requests for the same object ID will return the same object.
 
  * If you need to be able to tell if an item exists in a DM, it can be handy to implement a `get()` method, that checks the DM's `cache` attribute before checking the underlying database, and finally using `preloadState()` to retrieve the now-loaded object.
 
 * Miscellaneous
 
  * `peak.storage.files.EditableFile` provides an easy way to transactionally alter the contents of a file small enough to be held in memory. It works with the PEAK transaction framework to ensure that existing data isn't lost in the case of a write failure or other error, and that rolled-back changes are in fact rolled back.
 
Up: IntroToPeak Previous: IntroToPeak/LessonTwo Next: IntroToPeak/LessonFour

PythonPowered
ShowText of this page
EditText of this page
FindPage by browsing, title search , text search or an index
Or try one of these actions: AttachFile, DeletePage, LikePages, LocalSiteMap, SpellCheck