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-18 21:27:56 and 2005-02-01 11:15:34 (spanning 18 versions)

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

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")

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

also accepts URLs on the command line by default. `commands.Bootstrap`
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

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
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

 
[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 = """

        newmsg.text = message.strip()
 
        storage.commitTransaction(self)
 
}}} To put a new object in our database, we ask the Data Manager for
a new "empty" object, using `newItem()`. (Actually, it can have a
preloaded default state, but we'll ignore that for now). Then we

== Storing a New Message: Modifying the Data Manager ==
 
OK, it's time to do some serious surgery on our Data Manager.
First, we need to exchange our `QueryDM` base class to a base
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.
 
Let's write the new `hello_storage.py`: {{{
Let's write the new `storage.py`: {{{
#!python
from peak.api import *
from hello_model import Message
from helloworld.model import Message
 
class MessageDM(storage.EntityDM):
 

        self._save(ob)
        return ob.forname
 
    def _save(ob):
    def _save(self,ob):
        self.data[ob.forname] = {'forname':ob.forname, 'text':ob.text}
}}}
 
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: {{{
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: {{{
 
% ./hello for Fred: Hi, guy!
% ./hello to Fred

 
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?
 
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: {{{
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)

especially since we will already be inside a transaction whenever we use
the data manager.
 
Okay, so let's fix up `hello_storage.py` to use `EditableFile`: {{{
Okay, so let's fix up `storage.py` to use `EditableFile`: {{{
#!python
from peak.api import *
from hello_model import Message
from peak.storage.files import EditableFile
from helloworld.model import Message
 
class MessageDM(storage.EntityDM):
 

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.
 
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: {{{
    def abortTransaction(self, txnSvc):
        del self.data
#!python
    def abortTransaction(self, ob):
        self._delBinding("data")
        super(MessageDM,self).abortTransaction(ob)
}}}
 

 
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
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:

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. But, before
we do that, we need to add a `__contains__` method to our DM, to
check whether a particular name exists in the "database": {{{
 
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 oid in self.data
 
}}}
 
Now we can update our `forCmd._run()` method in `helloworld.py`: {{{
 
Now we can update our `forCmd._run()` method in `commands.py`: {{{
#!python
    def _run(self):
 
        if len(self.argv)<2:

 
        msg.text = message
        storage.commitTransaction(self)
 
}}}
 
With this change, updating the database should still work: {{{

}}}
 
Sharp-eyed readers will notice that the `__contains__` method we wrote does
'''double''' the normal work for retrieving an item, because it actually "loads"
''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`.
In this way, we only retrieve the object once. To support this (and
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. So, our `get()` method can load the
state from our "database", and then preload it into the object it
returns.
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
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.preloadState(oid, self.data[oid])
 
        return default
 
}}}
DM's have a `cache` attribute that holds on to currently loaded objects,
so that multiple requests for a given object ID, will always return the
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.
 

 
Let's finish out our refactoring by updating `forCmd` to use our new
`get()` method: {{{
 
#!python
    def _run(self):
 
        if len(self.argv)<2:

            msg.forname = forname
 
        msg.text = message
        storage.commitTransaction(self)
 
        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!)
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.
 

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