[ZODB-Dev] Python properties on Persistent objects

Mikko Ohtamaa mikko at redinnovation.com
Wed Dec 16 19:12:00 EST 2009


Hi,

I need to have little clarification should properties work on
Persistent objects. I am running ZODB 3.8.4 on Plone 3.3.

I am using plone.behavior and adapters to retrofit objects with a new
behavior (HeaderBehavior object). This object is also editable through
z3c.form interface. z3c.form requires a context variable on the object
e.g. to look up dynamic vocabularies. To avoid having this
object.context attribute to be peristent (as it's known every time by
the factory method of the adapter which creates/look-ups
HeaderBehavior) I tried to spoof context variable using properties and
internal volatile variable. This was a trick I learnt somewhere
(getpaid.core?)

It didn't work. Looks like Persistent class is not aware of properties
and interprets property set as a transaction start. So even though I
thought I was having a volatile context, my Undo log kept having new
write entries on every page read for pages having HeaderBehavior
objects in their annotations. Data.fs grew steadily with small writes
and there was a whole bunch of ConflictErrors.

I think I found a workaround. By overriding __setattr__ and not
letting through properties and volatile attribute set to Persistent
__setattr__ you can avoid the problem.

Is my rationale correct? Should Persistent behave well with
properties? I found only one discussion in Google, circa 2004.

Sample code below:

from persistent import Persistent
class VolatileContext(Persistent):
    """ Mix-in class to provide non-persistent context attribute to
persistent classes.

    Some subsystems (e.g. z3c.forms) expect objects to have a context
reference to parent/site/whatever.
    However, storing this back-reference persistenly is not needed, as
the factory
    method will always know the context.

    This helper class creates a context property which is volatile
(never persistent),
    but can be still set on the object after creation or after database load.
    """

    zope.interface.implements(IVolatileContext)

    def _set_context(self, context):
        self._v_context = context

    def _get_context(self):
        return self._v_context

    def _set_factory(self, factory):
        self._v_factory = factory

    def _get_factory(self):
        return self._v_factory

    # http://docs.python.org/library/functions.html#property
    context = property(_get_context, _set_context)

    factory = property(_get_factory, _set_factory)

    def save(self):
        """ """
        self.factory.makePersistent(self)

    def __setattr__(self, name, value):
        if name not in ("context", "factory", "_v_context", "_v_factory"):
            Persistent.__setattr__(self, name, value)
            print "leaking " + name + " " + str(value)
        else:
            print "property set " + name + " " + str(value)
            object.__setattr__(self, name, value)



class AnnotationPersistentFactory(object):
    """ A factory pattern to manufacture persistent objects stored
within the parent object annotations.

    Until the first write, the default (non-persistent) object is
return. This prevents
    possible situations where database read could cause write.

    The first write must call
AnnotationPersistentFactory.makePersistent(object).
    Alternative, you can call AnnotationPersistentFactory.makePersistent(object)
    when entering the editing interface for the first time.

    After the first write, the saved persistent object is return.
    """


    def __init__(self, persistent_class, key):
        """
        @param persistent_class: Class reference / factory method
which will create new objects.
            Created classes must conform VolatileContext interface

        @param key: ASCII string, Key name used with IAnnotations
        """
        self.persistent_class = persistent_class
        self.key = key
        self._assertProperlySetUp()

    def _assertProperlySetUp(self):
        """
        Check that the framework is properly set up
        """
        assert callable(self.persistent_class), "Factory is missing"

        assert hasattr(self.persistent_class, "context"), "The
persistent object must support volatile context interface"

        assert self.key is not None, "You must give the annotations key"

    def makePersistent(self, object):
        """ Write created persistent object to the database.

        This will store the object on the annotations of its context.
        """
        assert isinstance(object, self.persistent_class), "Object %s
was not type of %s" % (str(object), str(self.persistent_class))
        annotations = IAnnotations(object.context)
        annotations[self.key] = object

    def __call__(self, context):
        """ Called by Zope framework when doing a factory call.

        Usually this class is refered as <adapter factory=""> and
        this method creates a new, read-only, persistent object.
        """

        annotations = IAnnotations(context)

        if not self.key in annotations:
            # Construct a new (default) instance
            print "Created"
            object = self.persistent_class()
        else:
            # Return the object stored previously
            print "Found"
            object = annotations[self.key]

        # Set volatile context reference
        object.context = context
        object.factory = self

        return object

-Mikko


More information about the ZODB-Dev mailing list