# along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+"""provides a class called AttributeManager that can attach history-aware attributes to object
+instances. AttributeManager-enabled object attributes can be scalar or lists. In both cases, the "change
+history" of each attribute is available via the AttributeManager in a unit called a "history
+container". Via the history container, which can be a scalar or list based container,
+the attribute can be "committed", meaning whatever changes it has are registered as the current value,
+or "rolled back", which means the original "committed" value is restored; in both cases
+the accumulated history is removed.
+
+The change history is represented as three lists, the "added items", the "deleted items",
+and the "unchanged items". In the case of a scalar attribute, these lists would be zero or
+one element in length. for a list based attribute, these lists are of arbitrary length.
+"unchanged items" represents the assigned value or appended values on the attribute either
+with "history tracking" disabled, or have been "committed". "added items" represent new
+values that have been assigned or appended to the attribute. "deleted items" represents the
+the value that was previously "unchanged", but has been de-assigned or removed from the attribute.
+
+AttributeManager can also assign a "callable" history container to an object's attribute,
+which is invoked when first accessed, to provide the object's "committed" value.
+
+The package includes functions for managing "bi-directional" object relationships as well
+via the ListBackrefExtension, OTMBackrefExtension, and MTOBackrefExtension objects.
+"""
import sqlalchemy.util as util
-import weakref
class SmartProperty(object):
- """attaches AttributeManager functionality to the property accessors of a class. all
- instances of the class will retrieve and modify their properties via an
- AttributeManager."""
+ """Provides a property object that will communicate set/get/delete operations
+ to an AttributeManager. SmartProperty objects are constructed by the
+ create_prop method on AttributeManger, which can be overridden to provide
+ subclasses of SmartProperty.
+ """
def __init__(self, manager):
self.manager = manager
def attribute_registry(self):
return property(get_prop, set_prop, del_prop)
class PropHistory(object):
- """manages the value of a particular scalar attribute on a particular object instance."""
+ """Used by AttributeManager to track the history of a scalar attribute
+ on an object instance. This is the "scalar history container" object.
+ Has an interface similar to util.HistoryList
+ so that the two objects can be called upon largely interchangeably."""
# make our own NONE to distinguish from "None"
NONE = object()
def __init__(self, obj, key, extension=None, **kwargs):
return []
class ListElement(util.HistoryArraySet):
- """manages the value of a particular list-based attribute on a particular object instance."""
+ """Used by AttributeManager to track the history of a list-based object attribute.
+ This is the "list history container" object.
+ Subclasses util.HistoryArraySet to provide "onchange" event handling as well
+ as a plugin point for BackrefExtension objects."""
def __init__(self, obj, key, data=None, extension=None, **kwargs):
self.obj = obj
self.key = key
return res
class CallableProp(object):
- """allows the attaching of a callable item, representing the future value
- of a particular attribute on a particular object instance, to
- the AttributeManager. When the attributemanager
- accesses the object attribute, either to get its history or its real value, the __call__ method
- is invoked which runs the underlying callable_ and sets the new value to the object attribute
- via the manager, at which point the CallableProp itself is dereferenced."""
+ """Used by AttributeManager to allow the attaching of a callable item, representing the future value
+ of a particular attribute on a particular object instance, to an attribute on an object.
+ This is the "callable history container" object.
+ When the attributemanager first accesses the object attribute, either to get its history or
+ its real value, the __call__ method
+ is invoked which runs the underlying callable_ and sets the new value to the object attribute,
+ at which point the CallableProp itself is dereferenced."""
def __init__(self, manager, callable_, obj, key, uselist = False, live = False, **kwargs):
self.manager = manager
self.callable_ = callable_
pass
class AttributeExtension(object):
+ """an abstract class which specifies an "onadd" or "ondelete" operation
+ to be attached to an object property."""
def append(self, obj, child):
pass
def delete(self, obj, child):
# getattr(child, self.key).append(obj)
class AttributeManager(object):
- """maintains a set of per-attribute callable/history manager objects for a set of objects."""
+ """maintains a set of per-attribute history container objects for a set of objects."""
def __init__(self):
pass
def value_changed(self, obj, key, value):
- """subclasses override this method to provide functionality upon an attribute change of value."""
+ """subclasses override this method to provide functionality that is triggered
+ upon an attribute change of value."""
pass
def create_prop(self, key, uselist, **kwargs):
+ """creates a scalar property object, defaulting to SmartProperty, which
+ will communicate change events back to this AttributeManager."""
return SmartProperty(self).property(key, uselist)
def create_list(self, obj, key, list_, **kwargs):
+ """creates a history-aware list property, defaulting to a ListElement which
+ is a subclass of HistoryArrayList."""
return ListElement(obj, key, list_, **kwargs)
def create_callable(self, obj, key, func, uselist, **kwargs):
+ """creates a callable container that will invoke a function the first
+ time an object property is accessed. The return value of the function
+ will become the object property's new value."""
return CallableProp(self, func, obj, key, uselist, **kwargs)
def get_attribute(self, obj, key, **kwargs):
+ """returns the value of an object's scalar attribiute."""
try:
return self.get_history(obj, key, **kwargs).getattr()
except KeyError:
raise AttributeError(key)
def get_list_attribute(self, obj, key, **kwargs):
+ """returns the value of an object's list-based attribute."""
return self.get_history(obj, key, **kwargs)
def set_attribute(self, obj, key, value, **kwargs):
- if key == 'parent' and value is not None and value.__class__.__name__ != 'Comment':
- raise "wha?"
+ """sets the value of an object's attribute."""
self.get_history(obj, key, **kwargs).setattr(value)
self.value_changed(obj, key, value)
def delete_attribute(self, obj, key, **kwargs):
+ """deletes the value from an object's attribute."""
self.get_history(obj, key, **kwargs).delattr()
self.value_changed(obj, key, None)
def rollback(self, *obj):
+ """rolls back all attribute changes on the given list of objects,
+ and removes all history."""
for o in obj:
try:
attributes = self.attribute_history(o)
pass
def commit(self, *obj):
+ """commits all attribute changes on the given list of objects,
+ and removes all history."""
for o in obj:
try:
attributes = self.attribute_history(o)
except KeyError:
pass
- def remove(self, obj):
+ def remove(self, obj):
+ """not sure what this is."""
pass
def create_history(self, obj, key, uselist, callable_=None, **kwargs):
+ """creates a new "history" container for a specific attribute on the given object.
+ this can be used to override a class-level attribute with something different,
+ such as a callable. """
p = self.create_history_container(obj, key, uselist, callable_=callable_, **kwargs)
self.attribute_history(obj)[key] = p
return p
def get_history(self, obj, key, passive=False, **kwargs):
+ """returns the "history" container for the given attribute on the given object.
+ If the container does not exist, it will be created based on the class-level
+ history container definition."""
try:
return self.attribute_history(obj)[key].gethistory(passive=passive, **kwargs)
except KeyError, e:
return self.class_managed(obj.__class__)[key](obj, **kwargs).gethistory(passive=passive, **kwargs)
def attribute_history(self, obj):
+ """returns a dictionary of "history" containers corresponding to the given object.
+ this dictionary is attached to the object via the attribute '_managed_attributes'.
+ If the dictionary does not exist, it will be created."""
try:
attr = obj.__dict__['_managed_attributes']
except KeyError:
return attr
def reset_history(self, obj, key):
+ """removes the history object for the given attribute on the given object.
+ When the attribute is next accessed, a new container will be created via the
+ class-level history container definition."""
try:
del self.attribute_history(obj)[key]
except KeyError:
pass
def class_managed(self, class_):
+ """returns a dictionary of "history container definitions", which is attached to a
+ class. creates the dictionary if it doesnt exist."""
try:
attr = getattr(class_, '_class_managed_attributes')
except AttributeError:
def create_history_container(self, obj, key, uselist, callable_ = None, **kwargs):
+ """creates a new history container for the given attribute on the given object."""
if callable_ is not None:
return self.create_callable(obj, key, callable_, uselist=uselist, **kwargs)
elif not uselist:
return self.create_list(obj, key, list_, **kwargs)
def register_attribute(self, class_, key, uselist, callable_=None, **kwargs):
- # create a function, that will create this object attribute when its first called
+ """registers an attribute's behavior at the class level. This attribute
+ can be scalar or list based, and also may have a callable unit that will be
+ used to create the initial value. The definition for this attribute is
+ wrapped up into a callable which is then stored in the classes'
+ dictionary of "class managed" attributes. When instances of the class
+ are created and the attribute first referenced, the callable is invoked to
+ create the new history container. Extra keyword arguments can be sent which
+ will be passed along to newly created history containers."""
def createprop(obj):
if callable_ is not None:
func = callable_(obj)
class HistoryArraySet(UserList.UserList):
"""extends a UserList to provide unique-set functionality as well as history-aware
- functionality, including information about what list elements were modified,
- as well as rollback capability."""
+ functionality, including information about what list elements were modified
+ and commit/rollback capability. When a HistoryArraySet is created with or
+ without initial data, it is in a "committed" state. as soon as changes are made
+ to the list via the normal list-based access, it tracks "added" and "deleted" items,
+ which remain until the history is committed or rolled back."""
def __init__(self, data = None, readonly=False):
# stores the array's items as keys, and a value of True, False or None indicating
# added, deleted, or unchanged for that item
data array. this allows custom list classes to be used."""
return getattr(self.data, attr)
def set_data(self, data):
+ """sets the data for this HistoryArraySet to be that of the given data.
+ duplicates in the incoming list will be removed."""
# first mark everything current as "deleted"
for i in self.data:
self.records[i] = False
del self.data[i]
i -= 1
def history_contains(self, obj):
+ """returns true if the given object exists within the history
+ for this HistoryArrayList."""
return self.records.has_key(obj)
def __hash__(self):
return id(self)
except KeyError:
return False
def commit(self):
+ """commits the added values in this list to be the new "unchanged" values.
+ values that have been marked as deleted are removed from the history."""
for key in self.records.keys():
value = self.records[key]
if value is False:
else:
self.records[key] = None
def rollback(self):
+ """rolls back changes to this list to the last "committed" state."""
# TODO: speed this up
list = []
for key, status in self.records.iteritems():
for l in list:
self.append_nohistory(l)
def added_items(self):
+ """returns a list of items that have been added since the last "committed" state."""
return [key for key in self.data if self.records[key] is True]
def deleted_items(self):
+ """returns a list of items that have been deleted since the last "committed" state."""
return [key for key, value in self.records.iteritems() if value is False]
def unchanged_items(self):
+ """returns a list of items that have not been changed since the last "committed" state."""
return [key for key in self.data if self.records[key] is None]
def append_nohistory(self, item):
+ """appends an item to the list without affecting the "history"."""
if not self.records.has_key(item):
self.records[item] = None
self.data.append(item)
def remove_nohistory(self, item):
+ """removes an item from the list without affecting the "history"."""
if self.records.has_key(item):
del self.records[item]
self.data.remove(item)