Despite what it says on the tin, fallback fields are not a full prototype inheritance system. I wrote them to solve a particular problem, and the prototype inheritance idea is similar enough to help with understanding them, but it's not the same thing. So let's start instead with what fallback fields are for.
Motivation and overview
I designed fallback fields for an application for making reports containing chart graphics (line charts, bar charts, etc). Users of this system get given some predefined charts, which are configured by an administrator for their account. The fallback fields are the result of the interaction of two partially contradictory requirements:
- Users can (re)configure their predefined charts, changing everything from the colour scheme through to which data sources the chart is reporting on.
- Admins can push changes to the predefined charts (e.g., adding a new data source to a line chart).
These requirements are in partial conflict in the case where both a user and an admin make changes to the same predefined chart: whose changes take precedence? Prototype inheritance of individual attributes answers this question: the user gets the values they have set on any attributes they have touched, but they get the admin's version of any attributes they didn't explicitly set themselves. But this produces a new problem: what should we do when the changes are to different attributes but are inconsistent (e.g., the admin removes a data source, and the user configures the legend entry for that data source)?
Fallback fields provide some tools to handle this situation, by disconnecting an object from its prototype: this copies all fallback attribute values from the prototype into the object itself, and removes the prototype. The result is that further updates to the prototype will no longer be reflected in the (now disconnected) object. (In the case of the charts, when the user assigns to an attribute that can be in conflict with another we can disconnect the chart. This still allows them to freely edit attributes that cannot conflict with admin changes, for instance the position of the chart legend.)
One final note: fallback fields were designed for use with Django models, for which field attributes are always present
on the objects but may have value
None. On a Django model, a fallback field should access the prototype if the local
None; outside of the Django context, it may be more appropriate to access the prototype if the attribute
has not been set on the object (thus allowing
None as an attribute value). The implementation allows both
variants, via a flag.
First, a non-Django example:
class SimpleDoc(FallbackFieldsMixin, object): title = FallbackProperty('_title') content = FallbackProperty('_content', on_edit_disconnect=True) def __init__(self, prototype=None): self.prototype = prototype
content will inherit the
prototype's value, but if
content is ever set the object will be disconnected from its prototype (see the tests for
FallbackProperty allows getters and setters like the builtin
@property decorator. The getter is a bit different
though: you provide a function that takes an argument, which will either come from the local object or its prototype.
If you redefine the setter it becomes your responsibility to set the object's attribute. The property will handle
disconnecting for you though, if you use
class AlwaysTitledDoc(FallbackFieldsMixin, object): title = FallbackProperty('_title') content = FallbackProperty('_content', on_edit_disconnect=True) @title.fallback_getter def title(self, title_or_none): return title_or_none or '<untitled>' @title.setter def title(self, new_title): if not new_title or not new_title.isupper(): raise ValueError(u'Invalid title (must be non-empty and start with a capital)') self._title = new_title @content.setter def content(self, new_content): """ Despite this setter, the object *will* be disconnected if you assign to its content. """ self._content = new_content.strip() def __init__(self, prototype=None): self.prototype = prototype
Things look very similar in the Django context, except that (a) you have to explicitly define the Django fields (of
course), and (b) you need to tell
FallbackProperty to use
None as the fallback marker:
class DjangoDoc(DjangoFallbackFieldsMixin, models.Model): prototype = ForeignKey('DjangoDoc', null=True) _title = CharField(max_length=200, db_column='title', null=True) _content = Text(db_column='content', null=True) title = FallbackProperty('_title', fallback_on_none=True) content = FallbackProperty('_content', on_edit_disconnect=True, fallback_on_none=True)
Compared to ordinary attribute access, fallback fields will be hellishly inefficient. If performance matters to you, just don't.
It seems unlikely anyone will try, but just in case: don't reuse the same
FallbackPropertyobjects within a single class. For example:
should_work = FallbackProperty('_title') will_not_work = FallbackProperty('_content') class NormalDoc(FallbackFieldsMixin, object): title = should_work content = FallbackProperty('_content') class WierdDoc(FallbackFieldsMixin, object): title = should_work content = will_not_work other_content = will_not_work
The reason is that a
FallbackPropertyneeds to know what attribute it 'lives under', to know what to look up on the prototype. The
should_workobject above keeps its name on
NormalDocseparate from its name on
will_not_workcannot keep track of whether it is accessed as