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. The Basics ========== Internally, two models are used: Object, and Property. Typically, you will create a new root object (without any prototype) as a starting point. >>> 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 'http://static.example.com/images/unknown_avatar.jpg' >>> adam.get_property("avatar") 'http://static.example.com/images/unknown_avatar.jpg' >>> print adam.get_property("invalid") None 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") None >>> user.avatar 'http://static.example.com/images/unknown_avatar.jpg' 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 installed. ``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") False 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") False >>> # 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.