This is a project that implements Steve Yegge's description of the
Properties Pattern  using Django. Particular emphasis is placed on
good Python and Django integration.
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.
>>> 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"
>>> 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.
>>> print adam.get_property("avatar")
``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
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
``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)
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.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.