universal /

Filename Size Date modified Message
6.1 KB
This is a project that implements Steve Yegge's description of the
Properties Pattern [0] using Django. Particular emphasis is placed on
good Python and Django integration.

The Basics

Internally, two models are used: Object, and Property. Typically, you
will create a new root object (without any prototype) as a starting

    >>> from universal.models import Object
    >>> root_object = Object.objects.create()

You can then create children via Object.make_instance(), which is a
shortcut for Object.instances.create().

    >>> child = root_object.make_instance()
    >>> child2 = root_object.instances.create()

You shouldn't use this for tree structures, though; the relationship
isn't truly parent/child. Instead, instances should be specializations
of more general objects.

    >>> user = root_object.make_instance()
    >>> adam = user.make_instance()

The first time you use a property, you must call Object.set_property()
to tell the model that that attribute should be treated as a
persistent, inheritable attribute. From that point onwards, however,
you may use regular assignment syntax.

This means that you will usually define a 'schema' of undefined
attributes on general objects, then assign to them in the instances.

    >>> user.set_property("full_name")
    >>> user.set_property("username")
    >>> adam.full_name = "Adam Gomaa"
    >>> adam.username = "AdamKG"

Note that these things are being persisted immediately - there is very
little reason to call Object.save(), as the actual changes are in
Property objects that are being created as soon as you call
Object.set_property() or set a property-based attribute.

If you attempt to access a property that is not defined in an
instance, the prototype chain is climbed until the property is found,
or an AttributeError is raised. You can use ``Object.get_property()``,
which will return None if nothing is found, if you want to avoid
having to catch AttributeError.

    >>> user.avatar = "http://static.example.com/images/unknown_avatar.jpg"
    >>> adam.avatar
    >>> adam.get_property("avatar")
    >>> print adam.get_property("invalid")

There are a few restrictions on property names:

* They must not start with underscores
* They must not be one of 'prototype', 'prototype_id', or 'as_sql' due to implementation limitations
* They should not be objects other than strings

They are limited in length only by the database.

Deleting Properties in Instances

Properties can be deleted from instances via
``Object.delete_property()``. This actually creates a ``Property``
object with a ``Property.DELETED`` status. The property will, of
course, be untouched on the prototype.

    >>> adam.delete_property("avatar")
    >>> print adam.get_property("avatar")
    >>> user.avatar

Property Implementation

``Property`` is a relatively simple model. It consists of four fields:

* A ``ForeignKey`` to the ``Object`` instance it is associated with
* ``Property.key``, the property name
* ``Property.value``, the property value
* ``Property.type``, an indication of which type the value is, used for deserialization

Property Types

Properties can have one of six types. ``Property.set_value()`` and
``Property.get_value()`` use the type to determine how to serialize
and retrieve objects.

The default type is ``Property.PICKLE``, which pickles the value. This
handles a wide range of Python objects very well with a minimal amount
of complexity.

``Property.RAW_VALUE`` tells the property to do no modifications to
the value; it is persisted and returned as a string.

``Property.DELETE`` is a special value that indicates that the
property has been deleted on the instance, even if it exists higher up
the prototype chain.

``Property.MODEL`` and ``Property.INSTANCE`` are Django-specific
types, which can only be used with subclasses of
``django.db.models.Model`` and their instances, respectively. It
requires the ``django.contrib.contenttypes``application to be

``Property.BEHAVIOR`` is described below.

Advanced: Behavior Classes

In Python's class-based object system, behavior can be added as
methods on the class. To some extent, you can do the same with
instances of ``Object``, however, you'll soon find a few problems.

First, you cannot make bound methods. There's no way of building a
function with signature of ``(self, *args, **kwargs)`` without passing
the object during calls, eg, ``adam.authenticate(adam, password)``.

Second, only simplistic functions can be pickled. They must be
top-level functions in a module; closures will not work.

Instead, ``universal.behavior.Behavior`` subclasses can be
used. Although I cannot provide bound methods (at least, I don't know
enough of Python's object system internals to figure it out),
accessing attributes that are Behavior subclasses will instantiate the
Behavior class and make the instance available under ``self.object``::

    >>> import universal.behavior
    >>> class UserBehavior(universal.behavior.Behavior):
    ...     def authenticate(self, password):
    ...         import hashlib
    ...         return hashlib.sha1(password).hexdigest == self.object.hashed_password
    >>> adam.set_property("behavior", UserBehavior)
    >>> adam.behavior.authenticate("password")

Note that the choice of "behavior" as a property name is not
significant. In fact, splitting behavior across different classes and
attributes is fine::

    >>> adam.set_property("auth_behavior", UserBehavior)
    >>> adam.auth_behavior.authenticate("password")
    >>> # adam.set_property("friendship_behavior", SomeOtherBehaviorClass)

``Behavior`` classes need not be subclasses of
``universal.behavior.Behavior``, but if they are not, you must pass
the ``Property.BEHAVIOR`` type.