]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
merged attributes rewrite
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 15 Jun 2006 15:53:00 +0000 (15:53 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 15 Jun 2006 15:53:00 +0000 (15:53 +0000)
16 files changed:
CHANGES
doc/build/content/document_base.myt
lib/sqlalchemy/attributes.py
lib/sqlalchemy/orm/dependency.py
lib/sqlalchemy/orm/mapper.py
lib/sqlalchemy/orm/properties.py
lib/sqlalchemy/orm/session.py
lib/sqlalchemy/orm/unitofwork.py
lib/sqlalchemy/util.py
setup.py
test/base/attributes.py
test/orm/inheritance3.py
test/orm/manytomany.py
test/orm/mapper.py
test/orm/objectstore.py
test/perf/massload.py

diff --git a/CHANGES b/CHANGES
index 5a5f24e70a7eba9c374dd98b47bfdde0c3ce3efa..8c17e72dd9424e4b14a93ced6169cf7be87e19a2 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -1,9 +1,15 @@
 0.2.3
 - overhaul to mapper compilation to be deferred.  this allows mappers
 to be constructed in any order, and their relationships to each
-other are compiled when the mappers are first used..
+other are compiled when the mappers are first used.
 - fixed a pretty big speed bottleneck in cascading behavior particularly
 when backrefs were in use
+- the attribute instrumentation module has been completely rewritten; its
+now a large degree simpler and clearer, slightly faster.  the "history"
+of an attribute is no longer micromanaged with each change and is
+instead part of a "CommittedState" object created when the 
+instance is first loaded.  HistoryArraySet is gone, the behavior of
+list attributes is now more open ended (i.e. theyre not sets anymore).
 - py2.4 "set" construct used internally, falls back to sets.Set when
 "set" not available/ordering is needed.
 - "foreignkey" argument to relation() can also be a list.  fixed
index f88a836ffc065298858e33ebdc7ce919ceb68df1..21f9f87803d3615d529ea17f08cfe635239ab270 100644 (file)
@@ -24,7 +24,7 @@
     onepage='documentation'
     index='index'
     title='SQLAlchemy 0.2 Documentation'
-    version = '0.2.2'
+    version = '0.2.3'
 </%attr>
 
 <%method title>
index 07b395fee0de5c5fddfe56ebf3c30d7ac817f74a..6444cd1abc36df8f80970fc44fab60786a0315a9 100644 (file)
 # This module is part of SQLAlchemy and is released under
 # the MIT License: http://www.opensource.org/licenses/mit-license.php
 
-"""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 GenericBackrefExtension object.
-"""
-
 import util
 import weakref
-from exceptions import *
 
-class SmartProperty(object):
-    """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, key, uselist, callable_, typecallable, **kwargs):
+class InstrumentedAttribute(object):
+    """a property object that instruments attribute access on object instances.  All methods correspond to 
+    a single attribute on a particular class."""
+    def __init__(self, manager, key, uselist, callable_, typecallable, trackparent=False, extension=None, **kwargs):
         self.manager = manager
         self.key = key
         self.uselist = uselist
         self.callable_ = callable_
         self.typecallable= typecallable
-        self.kwargs = kwargs
-    def init(self, obj, attrhist=None):
-        """creates an appropriate ManagedAttribute for the given object and establishes
-        it with the object's list of managed attributes."""
-        if self.callable_ is not None:
-            func = self.callable_(obj)
-        else:
-            func = None
-        return self.manager.create_managed_attribute(obj, self.key, self.uselist, callable_=func, attrdict=attrhist, typecallable=self.typecallable, **self.kwargs)
+        self.trackparent = trackparent
+        self.extensions = util.to_list(extension or [])
+
     def __set__(self, obj, value):
-        self.manager.set_attribute(obj, self.key, value)
+        self.set(None, obj, value)
     def __delete__(self, obj):
-        self.manager.delete_attribute(obj, self.key)
+        self.delete(None, obj)
     def __get__(self, obj, owner):
         if obj is None:
             return self
-        if self.uselist:
-            return self.manager.get_list_attribute(obj, self.key)
-        else:
-            return self.manager.get_attribute(obj, self.key)
-    def setattr_clean(self, obj, value):
-        """sets an attribute on an object without triggering a history event"""
-        h = self.manager.get_history(obj, self.key)
-        h.setattr_clean(value)
-    def append_clean(self, obj, value):
-        """appends a value to a list-based attribute without triggering a history event."""
-        h = self.manager.get_history(obj, self.key)
-        h.append_nohistory(value)
+        return self.get(obj)
 
-class ManagedAttribute(object):
-    """base class for a "managed attribute", which is attached to individual instances
-    of a class mapped to the keyname of the property, inside of a dictionary which is
-    attached to the object via the propertyname "_managed_attributes".  Attribute access
-    which occurs through the SmartProperty property object ultimately calls upon 
-    ManagedAttribute objects associated with the instance via this dictionary."""
-    def __init__(self, obj, key):
-        self.__obj = weakref.ref(obj)
-        self.key = key
-    def __getstate__(self):
-        return {'key':self.key, 'obj':self.obj}
-    def __setstate__(self, d):
-        self.key = d['key']
-        self.__obj = weakref.ref(d['obj'])
-    obj = property(lambda s:s.__obj())
-    def value_changed(self, *args, **kwargs):
-        self.obj._managed_value_changed = True
-        self.do_value_changed(*args, **kwargs)
-    def history(self, **kwargs):
-        return self
-    def plain_init(self, *args, **kwargs):
-        pass
     def hasparent(self, item):
-        return item.__class__._attribute_manager.attribute_history(item).get(('_hasparent_', self.obj.__class__, self.key))
+        """returns True if the given item is attached to a parent object 
+        via the attribute represented by this InstrumentedAttribute."""
+        return item._state.get(('hasparent', self))
+        
     def sethasparent(self, item, value):
+        """sets a boolean flag on the given item corresponding to whether or not it is
+        attached to a parent object via the attribute represented by this InstrumentedAttribute."""
         if item is not None:
-            item.__class__._attribute_manager.attribute_history(item)[('_hasparent_', self.obj.__class__, self.key)] = value
+            item._state[('hasparent', self)] = value
 
-class ScalarAttribute(ManagedAttribute):
-    """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, trackparent=False, **kwargs):
-        ManagedAttribute.__init__(self, obj, key)
-        self.orig = ScalarAttribute.NONE
-        self.extension = extension
-        self.trackparent = trackparent
-    def clear(self):
-        del self.obj.__dict__[self.key]
-    def history_contains(self, obj):
-        return self.orig is obj or self.obj.__dict__[self.key] is obj
-    def setattr_clean(self, value):
-        self.obj.__dict__[self.key] = value
-    def delattr_clean(self):
-        del self.obj.__dict__[self.key]
-    def getattr(self, **kwargs):
-        return self.obj.__dict__[self.key]
-    def setattr(self, value, **kwargs):
-        #if isinstance(value, list):
-        #    raise InvalidRequestError("assigning a list to scalar property '%s' on '%s' instance %d" % (self.key, self.obj.__class__.__name__, id(self.obj)))
-        orig = self.obj.__dict__.get(self.key, None)
-        if orig is value:
-            return
-        if self.orig is ScalarAttribute.NONE:
-            self.orig = orig
-        self.obj.__dict__[self.key] = value
-        if self.trackparent:
-            if value is not None:
-                self.sethasparent(value, True)
-            if orig is not None:
-                self.sethasparent(orig, False)
-        if self.extension is not None:
-            self.extension.set(self.obj, value, orig)
-        self.value_changed(orig, value)
-    def delattr(self, **kwargs):
-        orig = self.obj.__dict__.get(self.key, None)
-        if self.orig is ScalarAttribute.NONE:
-            self.orig = orig
-        self.obj.__dict__[self.key] = None
-        if self.trackparent:
-            self.sethasparent(orig, False)
-        if self.extension is not None:
-            self.extension.set(self.obj, None, orig)
-        self.value_changed(orig, None)
-    def append(self, obj):
-        self.setattr(obj)
-    def remove(self, obj):
-        self.delattr()
-    def rollback(self):
-        if self.orig is not ScalarAttribute.NONE:
-            self.obj.__dict__[self.key] = self.orig
-            self.orig = ScalarAttribute.NONE
-    def commit(self):
-        self.orig = ScalarAttribute.NONE
-    def do_value_changed(self, oldvalue, newvalue):
-        pass
-    def added_items(self):
-        if self.orig is not ScalarAttribute.NONE:
-            return [self.obj.__dict__.get(self.key)]
+    def get_history(self, obj, passive=False):
+        """returns a new AttributeHistory object for the given object for this 
+        InstrumentedAttribute's attribute."""
+        return AttributeHistory(self, obj, passive=passive)
+
+    def set_callable(self, obj, callable_):
+        """sets a callable function on the given object which will be executed when this attribute
+        is next accessed.  if the callable is None, then initializes the attribute with an empty value
+        (which overrides any class-level callables that might be on this attribute.)"""
+        if callable_ is None:
+            self.initialize(obj)
         else:
-            return []
-    def deleted_items(self):
-        if self.orig is not ScalarAttribute.NONE and self.orig is not None:
-            return [self.orig]
+            obj._state[('callable', self)] = callable_
+
+    def reset(self, obj):
+        """removes any per-instance callable functions corresponding to this InstrumentedAttribute's attribute
+        from the given object, and removes this InstrumentedAttribute's
+        attribute from the given object's dictionary."""
+        try:
+            del obj._state[('callable', self)]
+        except KeyError:
+            pass
+        self.clear(obj)
+    
+    def clear(self, obj):
+        """removes this InstrumentedAttribute's attribute from the given object's dictionary. subsequent calls to
+        getattr(obj, key) will raise an AttributeError by default."""
+        try:
+            del obj.__dict__[self.key]
+        except KeyError:
+            pass
+            
+    def _get_callable(self, obj):
+        if obj._state.has_key(('callable', self)):
+            return obj._state[('callable', self)]
+        elif self.callable_ is not None:
+            return self.callable_(obj)
         else:
-            return []
-    def unchanged_items(self):
-        if self.orig is ScalarAttribute.NONE:
-            return [self.obj.__dict__.get(self.key)]
+            return None
+            
+    def _blank_list(self):
+        if self.typecallable is not None:
+            return self.typecallable()
         else:
             return []
 
-class ListAttribute(util.HistoryArraySet, ManagedAttribute):
-    """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, trackparent=False, typecallable=None, **kwargs):
-        ManagedAttribute.__init__(self, obj, key)
-        self.extension = extension
-        self.trackparent = trackparent
-        # if we are given a list, try to behave nicely with an existing
-        # list that might be set on the object already
-        try:
-            list_ = obj.__dict__[key]
-            if list_ is data:
-                raise InvalidArgumentError("Creating a list element passing the object's list as an argument")
+    def _adapt_list(self, data):
+        if self.typecallable is not None:
+            t = self.typecallable()
             if data is not None:
-                for d in data:
-                    list_.append(d)
+                [t.append(x) for x in data]
+            return t
+        else:
+            return data
+            
+    def initialize(self, obj):
+        if self.uselist:
+            l = InstrumentedList(self, obj, self._blank_list())
+            obj.__dict__[self.key] = l
+            return l
+        else:
+            obj.__dict__[self.key] = None
+            return None
+            
+    def get(self, obj, passive=False, raiseerr=True):
+        """retrieves a value from the given object.  if a callable is assembled
+        on this object's attribute, and passive is False, the callable will be executed
+        and the resulting value will be set as the new value for this attribute."""
+        try:
+            return obj.__dict__[self.key]
         except KeyError:
-            if data is not None:
-                list_ = data
-            elif typecallable is not None:
-                list_ = typecallable()
+            state = obj._state
+            if state.has_key('trigger'):
+                trig = state['trigger']
+                del state['trigger']
+                trig()
+                return self.get(obj, passive=passive, raiseerr=raiseerr)
+                    
+            if self.uselist:
+                callable_ = self._get_callable(obj)
+                if callable_ is not None:
+                    if passive:
+                        return None
+                    l = InstrumentedList(self, obj, self._adapt_list(callable_()), init=False)
+                    orig = state.get('original', None)
+                    if orig is not None:
+                        orig.commit_attribute(self, obj, l)
+                else:
+                    l = InstrumentedList(self, obj, self._blank_list(), init=False)
+                obj.__dict__[self.key] = l
+                return l
             else:
-                list_ = []
-            obj.__dict__[key] = list_
-        util.HistoryArraySet.__init__(self, list_, readonly=kwargs.get('readonly', False))
-    def do_value_changed(self, obj, key, item, listval, isdelete):
-        pass    
-    def setattr(self, value, **kwargs):
-        self.obj.__dict__[self.key] = value
-        self.set_data(value)
-    def delattr(self, value, **kwargs):
-        pass
-    def do_value_appended(self, item):
-        if self.trackparent:
-            self.sethasparent(item, True)
-        self.value_changed(self.obj, self.key, item, self, False)
-        if self.extension is not None:
-            self.extension.append(self.obj, item)
-    def do_value_deleted(self, item):
-        if self.trackparent:
-            self.sethasparent(item, False)
-        self.value_changed(self.obj, self.key, item, self, True)
-        if self.extension is not None:
-            self.extension.delete(self.obj, item)
+                callable_ = self._get_callable(obj)
+                if callable_ is not None:
+                    if passive:
+                        return None
+                    obj.__dict__[self.key] = self._adapt_list(callable_())
+                    orig = state.get('original', None)
+                    if orig is not None:
+                        orig.commit_attribute(self, obj)
+                    return obj.__dict__[self.key]
+                else:
+                    if raiseerr:
+                        # this is returning None for backwards compatibility.  I am considering
+                        # changing it to raise AttributeError, which would make object instances
+                        # act more like regular python objects, i.e. you dont set the attribute, you get
+                        # AttributeError when you call it.
+                        return None
+                        #raise AttributeError(self.key)
+                    else:
+                        return None
         
-class TriggeredAttribute(ManagedAttribute):
-    """Used by AttributeManager to allow the attaching of a callable item, representing the future value
-    of a particular attribute on a particular object instance, as the current attribute on an object. 
-    When accessed normally, its history() method is invoked to run the underlying callable, which
-    is then used to create a new ScalarAttribute or ListAttribute.  This new attribute object 
-    is then registered with the attribute manager to replace this TriggeredAttribute as the 
-    current ManagedAttribute."""
-    def __init__(self, manager, callable_, obj, key, uselist = False, live = False, **kwargs):
-        ManagedAttribute.__init__(self, obj, key)
-        self.manager = manager
-        self.callable_ = callable_
-        self.uselist = uselist
-        self.kwargs = kwargs
+    def set(self, event, obj, value):
+        """sets a value on the given object. 'event' is the InstrumentedAttribute that
+        initiated the set() operation and is used to control the depth of a circular setter
+        operation."""
+        if event is not self:
+            state = obj._state
+            if state.has_key('trigger'):
+                trig = state['trigger']
+                del state['trigger']
+                trig()
+            if self.uselist:
+                value = InstrumentedList(self, obj, value)
+            elif self.trackparent or len(self.extensions):
+                old = self.get(obj, raiseerr=False)
+            obj.__dict__[self.key] = value
+            state['modified'] = True
+            if not self.uselist:
+                if self.trackparent:
+                    if value is not None:
+                        self.sethasparent(value, True)
+                    if old is not None:
+                        self.sethasparent(old, False)
+                for ext in self.extensions:
+                    ext.set(event or self, obj, value, old)
+            
+    def delete(self, event, obj):
+        """deletes a value from the given object. 'event' is the InstrumentedAttribute that
+        initiated the delete() operation and is used to control the depth of a circular delete
+        operation."""
+        if event is not self:
+            try:
+                old = obj.__dict__[self.key]
+                del obj.__dict__[self.key]
+            except KeyError:
+                raise AttributeError(self.key)
+            obj._state['modified'] = True
+            if self.trackparent:
+                if old is not None:
+                    self.sethasparent(old, False)
+            for ext in self.extensions:
+                ext.delete(event or self, obj, old)
 
-    def clear(self):
-        self.plain_init(self.manager.attribute_history(self.obj))
-        
-    def plain_init(self, attrhist):
-        if not self.uselist:
-            p = self.manager.create_scalar(self.obj, self.key, **self.kwargs)
-            self.obj.__dict__[self.key] = None
+    def append(self, event, obj, value):
+        """appends an element to a list based element or sets a scalar based element to the given value.
+        Used by GenericBackrefExtension to "append" an item independent of list/scalar semantics.
+        'event' is the InstrumentedAttribute that initiated the append() operation and is used to control 
+        the depth of a circular append operation."""
+        if self.uselist:
+            if event is not self:
+                self.get(obj).append_with_event(value, event)
+        else:
+            self.set(event, obj, value)
+
+    def remove(self, event, obj, value):
+        """removes an element from a list based element or sets a scalar based element to None.
+        Used by GenericBackrefExtension to "remove" an item independent of list/scalar semantics.
+        'event' is the InstrumentedAttribute that initiated the remove() operation and is used to control 
+        the depth of a circular remove operation."""
+        if self.uselist:
+            if event is not self:
+                self.get(obj).remove_with_event(value, event)
         else:
-            p = self.manager.create_list(self.obj, self.key, None, **self.kwargs)
-        attrhist[self.key] = p
+            self.set(event, obj, None)
+
+    def append_event(self, event, obj, value):
+        """called by InstrumentedList when an item is appended"""
+        obj._state['modified'] = True
+        if self.trackparent:
+            self.sethasparent(value, True)
+        for ext in self.extensions:
+            ext.append(event or self, obj, value)
     
-    def __getattr__(self, key):
-        def callit(*args, **kwargs):
-            passive = kwargs.pop('passive', False)
-            return getattr(self.history(passive=passive), key)(*args, **kwargs)
-        return callit
+    def remove_event(self, event, obj, value):
+        """called by InstrumentedList when an item is removed"""
+        obj._state['modified'] = True
+        if self.trackparent:
+            self.sethasparent(value, False)
+        for ext in self.extensions:
+            ext.delete(event or self, obj, value)
+                
+class InstrumentedList(object):
+    """instruments a list-based attribute.  all mutator operations (i.e. append, remove, etc.) will fire off events to the 
+    InstrumentedAttribute that manages the object's attribute.  those events in turn trigger things like
+    backref operations and whatever is implemented by do_list_value_changed on InstrumentedAttribute.
     
-    def history(self, passive=False):
-        if not self.uselist:
-            if self.obj.__dict__.get(self.key, None) is None:
-                if passive:
-                    value = None
-                else:
-                    try:
-                        value = self.callable_()
-                    except AttributeError, e:
-                        # this catch/raise is because this call is frequently within an 
-                        # AttributeError-sensitive callstack
-                        raise AssertionError("AttributeError caught in callable prop:" + str(e.args))
-                self.obj.__dict__[self.key] = value
+    note that this list does a lot less than earlier versions of SA list-based attributes, which used HistoryArraySet.  
+    this list wrapper does *not* maintain setlike semantics, meaning you can add as many duplicates as 
+    you want (which can break a lot of SQL), and also does not do anything related to history tracking."""
+    def __init__(self, attr, obj, data, init=True):
+        self.attr = attr
+        # this weakref is to prevent circular references between the parent object
+        # and the list attribute, which interferes with immediate garbage collection.
+        self.__obj = weakref.ref(obj)
+        self.key = attr.key
+        self.data = data or attr._blank_list()
+        
+        # adapt to lists or sets automatically
+        if hasattr(self.data, 'append'):
+            self._data_appender = self.data.append
+        elif hasattr(self.data, 'add'):
+            self._data_appender = self.data.add
+            
+        if init:
+            for x in self.data:
+                self.__setrecord(x)
+                
+    def __getstate__(self):
+        """implemented to allow pickling, since __obj is a weakref."""
+        return {'key':self.key, 'obj':self.obj, 'data':self.data, 'attr':self.attr}
+    def __setstate__(self, d):
+        """implemented to allow pickling, since __obj is a weakref."""
+        self.key = d['key']
+        self.__obj = weakref.ref(d['obj'])
+        self.data = d['data']
+        self.attr = d['attr']
+        
+    obj = property(lambda s:s.__obj())
+
+    def unchanged_items(self):
+        """deprecated"""
+        return self.attr.get_history(self.obj).unchanged_items
+    def added_items(self):
+        """deprecated"""
+        return self.attr.get_history(self.obj).added_items
+    def deleted_items(self):
+        """deprecated"""
+        return self.attr.get_history(self.obj).deleted_items
+
+    def __iter__(self):
+        return iter(self.data)
+    def __repr__(self):
+        return repr(self.data)
+        
+    def __getattr__(self, attr):
+        """proxies unknown methods and attributes to the underlying
+        data array.  this allows custom list classes to be used."""
+        return getattr(self.data, attr)
+
+    def __setrecord(self, item, event=None):
+        self.attr.append_event(event, self.obj, item)
+        return True
 
-            p = self.manager.create_scalar(self.obj, self.key, **self.kwargs)
+    def __delrecord(self, item, event=None):
+        self.attr.remove_event(event, self.obj, item)
+        return True
+        
+    def append_with_event(self, item, event):
+        self.__setrecord(item, event)
+        self._data_appender(item)
+        
+    def append_without_event(self, item):
+        self._data_appender(item)
+        
+    def remove_with_event(self, item, event):
+        self.__delrecord(item, event)
+        self.data.remove(item)
+            
+    def append(self, item, _mapper_nohistory=False):
+        """fires off dependent events, and appends the given item to the underlying list.
+        _mapper_nohistory is a backwards compatibility hack; call append_without_event instead."""
+        if _mapper_nohistory:
+            self.append_without_event(item)
         else:
-            if not self.obj.__dict__.has_key(self.key) or len(self.obj.__dict__[self.key]) == 0:
-                if passive:
-                    value =  None
-                else:
-                    try:
-                        value = self.callable_()
-                    except AttributeError, e:
-                        # this catch/raise is because this call is frequently within an 
-                        # AttributeError-sensitive callstack
-                        raise AssertionError("AttributeError caught in callable prop:" + str(e.args))
-            else:
-                value = None
-            p = self.manager.create_list(self.obj, self.key, value, **self.kwargs)
-        if not passive:
-            # set the new history list as the new attribute, discards ourself
-            self.manager.attribute_history(self.obj)[self.key] = p
-            self.manager = None
-        return p
+            self.__setrecord(item)
+            self._data_appender(item)
+        
+    def clear(self):
+        if isinstance(self.data, dict):
+            self.data.clear()
+        else:
+            self.data[:] = self.attr._blank_list()
+            
+    def __getitem__(self, i):
+        return self.data[i]
+    def __setitem__(self, i, item):
+        if isinstance(i, slice):
+            self.__setslice__(i.start, i.stop, item)
+        else:
+            self.__setrecord(item)
+            self.data[i] = item
+    def __delitem__(self, i):
+        if isinstance(i, slice):
+            self.__delslice__(i.start, i.stop)
+        else:
+            self.__delrecord(self.data[i], None)
+            del self.data[i]
 
-    def commit(self):
-        pass
-    def rollback(self):
-        pass
+    def __lt__(self, other): return self.data <  self.__cast(other)
+    def __le__(self, other): return self.data <= self.__cast(other)
+    def __eq__(self, other): return self.data == self.__cast(other)
+    def __ne__(self, other): return self.data != self.__cast(other)
+    def __gt__(self, other): return self.data >  self.__cast(other)
+    def __ge__(self, other): return self.data >= self.__cast(other)
+    def __cast(self, other):
+       if isinstance(other, InstrumentedList): return other.data
+       else: return other
+    def __cmp__(self, other):
+       return cmp(self.data, self.__cast(other))
+    def __contains__(self, item): return item in self.data
+    def __len__(self): return len(self.data)
+    def __setslice__(self, i, j, other):
+        i = max(i, 0); j = max(j, 0)
+        [self.__delrecord(x) for x in self.data[i:]]
+        g = [a for a in list(other) if self.__setrecord(a)]
+        self.data[i:] = g
+    def __delslice__(self, i, j):
+        i = max(i, 0); j = max(j, 0)
+        for a in self.data[i:j]:
+            self.__delrecord(a)
+        del self.data[i:j]
+    def insert(self, i, item): 
+        if self.__setrecord(item):
+            self.data.insert(i, item)
+    def pop(self, i=-1):
+        item = self.data[i]
+        self.__delrecord(item)
+        return self.data.pop(i)
+    def remove(self, item): 
+        self.__delrecord(item)
+        self.data.remove(item)
+    def extend(self, item_list):
+        for item in item_list:
+            self.append(item)            
+    def __add__(self, other):
+        raise NotImplementedError()
+    def __radd__(self, other):
+        raise NotImplementedError()
+    def __iadd__(self, other):
+        raise NotImplementedError()
 
 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):
+    """an abstract class which specifies "append", "delete", and "set" 
+    event handlers to be attached to an object property."""
+    def append(self, event, obj, child):
         pass
-    def delete(self, obj, child):
+    def delete(self, event, obj, child):
         pass
-    def set(self, obj, child, oldchild):
+    def set(self, event, obj, child, oldchild):
         pass
         
 class GenericBackrefExtension(AttributeExtension):
-    """an attachment to a ScalarAttribute or ListAttribute which receives change events,
-    and upon such an event synchronizes a two-way relationship.  A typical two-way
+    """an extension which synchronizes a two-way relationship.  A typical two-way
     relationship is a parent object containing a list of child objects, where each
     child object references the parent.  The other are two objects which contain 
     scalar references to each other."""
     def __init__(self, key):
         self.key = key
-    def set(self, obj, child, oldchild):
+    def set(self, event, obj, child, oldchild):
+        if oldchild is child:
+            return
         if oldchild is not None:
-            prop = oldchild.__class__._attribute_manager.get_history(oldchild, self.key)
-            prop.remove(obj)
+            getattr(oldchild.__class__, self.key).remove(event, oldchild, obj)
         if child is not None:
-            prop = child.__class__._attribute_manager.get_history(child, self.key)
-            prop.append(obj)
-    def append(self, obj, child):
-        prop = child.__class__._attribute_manager.get_history(child, self.key)
-        prop.append(obj)
-    def delete(self, obj, child):
-        prop = child.__class__._attribute_manager.get_history(child, self.key)
-        prop.remove(obj)
+            getattr(child.__class__, self.key).append(event, child, obj)
+    def append(self, event, obj, child):
+        getattr(child.__class__, self.key).append(event, child, obj)
+    def delete(self, event, obj, child):
+        getattr(child.__class__, self.key).remove(event, child, obj)
 
-            
-class AttributeManager(object):
-    """maintains a set of per-attribute history container objects for a set of objects."""
-    def __init__(self):
-        pass
+class CommittedState(object):
+    """stores the original state of an object when the commit() method on the attribute manager
+    is called."""
+    def __init__(self, manager, obj):
+        self.data = {}
+        for attr in manager.managed_attributes(obj.__class__):
+            self.commit_attribute(attr, obj)
 
-    def do_value_changed(self, obj, key, value):
-        """subclasses override this method to provide functionality that is triggered 
-        upon an attribute change of value."""
-        pass
-        
-    def create_prop(self, class_, key, uselist, callable_, typecallable, **kwargs):
-        """creates a scalar property object, defaulting to SmartProperty, which 
-        will communicate change events back to this AttributeManager."""
-        return SmartProperty(self, key, uselist, callable_, typecallable, **kwargs)
-    def create_scalar(self, obj, key, **kwargs):
-        return ScalarAttribute(obj, key, **kwargs)
-    def create_list(self, obj, key, list_, typecallable=None, **kwargs):
-        """creates a history-aware list property, defaulting to a ListAttribute which
-        is a subclass of HistoryArrayList."""
-        return ListAttribute(obj, key, list_, typecallable=typecallable, **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 TriggeredAttribute(self, func, obj, key, uselist, **kwargs)
-        
-    def get_attribute(self, obj, key, **kwargs):
-        """returns the value of an object's scalar attribute, or None if
-        its not defined on the object (since we are a property accessor, this
-        is considered more appropriate than raising AttributeError)."""
-        h = self.get_unexec_history(obj, key)
-        try:
-            return h.getattr(**kwargs)
-        except KeyError:
-            return None
+    def commit_attribute(self, attr, obj, value=False):
+        if attr.uselist:
+            if value is not False:
+                self.data[attr.key] = [x for x in value]
+            elif obj.__dict__.has_key(attr.key):
+                self.data[attr.key] = [x for x in obj.__dict__[attr.key]]
+        else:
+            if value is not False:
+                self.data[attr.key] = value
+            elif obj.__dict__.has_key(attr.key):
+                self.data[attr.key] = obj.__dict__[attr.key]
+                        
+    def rollback(self, manager, obj):
+        for attr in manager.managed_attributes(obj.__class__):
+            if self.data.has_key(attr.key):
+                if attr.uselist:
+                    obj.__dict__[attr.key][:] = self.data[attr.key]
+                else:
+                    obj.__dict__[attr.key] = self.data[attr.key]
+            else:
+                del obj.__dict__[attr.key]
+                
+    def __repr__(self):
+        return "CommittedState: %s" % repr(self.data)
 
-    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):
-        """sets the value of an object's attribute."""
-        self.get_unexec_history(obj, key).setattr(value, **kwargs)
-    
-    def delete_attribute(self, obj, key, **kwargs):
-        """deletes the value from an object's attribute."""
-        self.get_unexec_history(obj, key).delattr(**kwargs)
+class AttributeHistory(object):
+    """calculates the "history" of a particular attribute on a particular instance, based on the CommittedState 
+    associated with the instance, if any."""
+    def __init__(self, attr, obj, passive=False):
+        self.attr = attr
+        # get the current state.  this may trigger a lazy load if
+        # passive is False.  
+        current = attr.get(obj, passive=passive, raiseerr=False)
+
+        # get the "original" value.  if a lazy load was fired when we got
+        # the 'current' value, this "original" was also populated just 
+        # now as well (therefore we have to get it second)
+        orig = obj._state.get('original', None)
+        if orig is not None:
+            original = orig.data.get(attr.key)
+        else:
+            original = None
+
+        if attr.uselist:
+            self._current = current
+        else:
+            self._current = [current]
+
+        if attr.uselist:
+            s = util.Set(original or [])
+            self._added_items = []
+            self._unchanged_items = []
+            self._deleted_items = []
+            if current:
+                for a in current:
+                    if a in s:
+                        self._unchanged_items.append(a)
+                    else:
+                        self._added_items.append(a)
+            for a in s:
+                if a not in self._unchanged_items:
+                    self._deleted_items.append(a)    
+        else:
+            if current is original:
+                self._unchanged_items = [current]
+                self._added_items = []
+                self._deleted_items = []
+            else:
+                self._added_items = [current]
+                if original is not None:
+                    self._deleted_items = [original]
+                else:
+                    self._deleted_items = []
+                self._unchanged_items = []
+
+    def __iter__(self):
+        return iter(self._current)
+    def added_items(self):
+        return self._added_items
+    def unchanged_items(self):
+        return self._unchanged_items
+    def deleted_items(self):
+        return self._deleted_items
+    def hasparent(self, obj):
+        """deprecated.  this should be called directly from the appropriate InstrumentedAttribute object."""
+        return self.attr.hasparent(obj)
         
+class AttributeManager(object):
+    """allows the instrumentation of object attributes.  AttributeManager is stateless, but can be
+    overridden by subclasses to redefine some of its factory operations."""
+
     def rollback(self, *obj):
-        """rolls back all attribute changes on the given list of objects, 
-        and removes all history."""
+        """retrieves the committed history for each object in the given list, and rolls back the attributes
+        each instance to their original value."""
         for o in obj:
+            orig = o._state.get('original')
+            if orig is not None:
+                orig.rollback(self, o)
+            else:
+                self._clear(o)
+    
+    def _clear(self, obj):
+        for attr in self.managed_attributes(obj.__class__):
             try:
-                attributes = self.attribute_history(o)
-                for hist in attributes.values():
-                    if isinstance(hist, ManagedAttribute):
-                        hist.rollback()
+                del obj.__dict__[attr.key]
             except KeyError:
                 pass
-            o._managed_value_changed = False
-
+                
     def commit(self, *obj):
-        """commits all attribute changes on the given list of objects, 
-        and removes all history."""
+        """creates a CommittedState instance for each object in the given list, representing
+        its "unchanged" state, and associates it with the instance.  AttributeHistory objects 
+        will indicate the modified state of instance attributes as compared to its value in this 
+        CommittedState object."""
         for o in obj:
-            try:
-                attributes = self.attribute_history(o)
-                for hist in attributes.values():
-                    if isinstance(hist, ManagedAttribute):
-                        hist.commit()
-            except KeyError:
-                pass
-            o._managed_value_changed = False
-    
+            o._state['original'] = CommittedState(self, o)
+            o._state['modified'] = False
+            
+    def managed_attributes(self, class_):
+        """returns an iterator of all InstrumentedAttribute objects associated with the given class."""
+        if not isinstance(class_, type):
+            raise repr(class_) + " is not a type"
+        for value in class_.__dict__.values():
+            if isinstance(value, InstrumentedAttribute):
+                yield value
+                
     def is_modified(self, object):
-        return getattr(object, '_managed_value_changed', False)
+        return object._state.get('modified', False)
         
-    def remove(self, obj):
-        """called when an object is totally being removed from memory"""
-        # currently a no-op since the state of the object is attached to the object itself
-        pass
-
-
     def init_attr(self, obj):
-        """sets up the _managed_attributes dictionary on an object.  this happens anyway 
-        when a particular attribute is first accessed on the object regardless
-        of this method being called, however calling this first will result in an elimination of 
-        AttributeError/KeyErrors that are thrown when get_unexec_history is called for the first
-        time for a particular key."""
-        d = {}
-        obj._managed_attributes = d
-        for value in obj.__class__.__dict__.values():
-            if not isinstance(value, SmartProperty):
-                continue
-            value.init(obj, attrhist=d).plain_init(d)
-
-    def get_unexec_history(self, obj, key):
-        """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 obj._managed_attributes[key]
-        except AttributeError, ae:
-            return getattr(obj.__class__, key).init(obj)
-        except KeyError, e:
-            return getattr(obj.__class__, key).init(obj)
+        """sets up the __sa_attr_state dictionary on the given instance.  This dictionary is
+        automatically created when the '_state' attribute of the class is first accessed, but calling
+        it here will save a single throw of an AttributeError that occurs in that creation step."""
+        setattr(obj, '_%s__sa_attr_state' % obj.__class__.__name__, {})
 
     def get_history(self, obj, key, **kwargs):
-        """accesses the appropriate ManagedAttribute container and calls its history() method.
-        For a TriggeredAttribute this will execute the underlying callable and return the
-        resulting ScalarAttribute or ListAttribute object.  For an existing ScalarAttribute
-        or ListAttribute, just returns the container."""
-        return self.get_unexec_history(obj, key).history(**kwargs)
-
-    def attribute_history(self, obj):
-        """returns a dictionary of ManagedAttribute 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.  If a 'trigger' has been placed on 
-        this object via the trigger_history() method, it will first be executed."""
-        try:
-            return obj._managed_attributes
-        except AttributeError:
-            obj._managed_attributes = {}
-            trigger = obj.__dict__.pop('_managed_trigger', None)
-            if trigger:
-                trigger()
-            return obj._managed_attributes
+        """returns a new AttributeHistory object for the given attribute on the given object."""
+        return getattr(obj.__class__, key).get_history(obj, **kwargs)
 
+    def get_as_list(self, obj, key, passive=False):
+        """returns an attribute of the given name from the given object.  if the attribute
+        is a scalar, returns it as a single-item list, otherwise returns the list based attribute.
+        if the attribute's value is to be produced by an unexecuted callable, 
+        the callable will only be executed if the given 'passive' flag is False.
+        """
+        attr = getattr(obj.__class__, key)
+        x = attr.get(obj, passive=passive)
+        if x is None:
+            return []
+        elif attr.uselist:
+            return x
+        else:
+            return [x]
+            
     def trigger_history(self, obj, callable):
-        """removes all ManagedAttribute instances from the given object and places the given callable
+        """clears all managed object attributes and places the given callable
         as an attribute-wide "trigger", which will execute upon the next attribute access, after
-        which the trigger is removed and the object re-initialized to receive new ManagedAttributes. """
+        which the trigger is removed."""
+        self._clear(obj)
         try:
-            del obj._managed_attributes
+            del obj._state['original']
         except KeyError:
             pass
-        obj._managed_trigger = callable
+        obj._state['trigger'] = callable
 
     def untrigger_history(self, obj):
-        del obj._managed_trigger
+        """removes a trigger function set by trigger_history.  does not restore the previous state of the object."""
+        del obj._state['trigger']
         
     def has_trigger(self, obj):
-        return hasattr(obj, '_managed_trigger')
+        """returns True if the given object has a trigger function set by trigger_history()."""
+        return obj._state.has_key('trigger')
             
-    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:
-            x = self.attribute_history(obj)[key]
-            x.clear()
-            del self.attribute_history(obj)[key]
-        except KeyError:
-            try:
-                del obj.__dict__[key]
-            except KeyError:
-                pass
+    def reset_instance_attribute(self, obj, key):
+        """removes any per-instance callable functions corresponding to given attribute key
+        from the given object, and removes this attribute from the given object's dictionary."""
+        attr = getattr(obj.__class__, key)
+        attr.reset(obj)
         
     def reset_class_managed(self, class_):
-        for value in class_.__dict__.values():
-            if not isinstance(value, SmartProperty):
-                continue
-            delattr(class_, value.key)
+        """removes all InstrumentedAttribute property objects from the given class."""
+        for attr in self.managed_attributes(class_):
+            delattr(class_, attr.key)
 
     def is_class_managed(self, class_, key):
-        return hasattr(class_, key) and isinstance(getattr(class_, key), SmartProperty)
+        """returns True if the given key correponds to an instrumented property on the given class."""
+        return hasattr(class_, key) and isinstance(getattr(class_, key), InstrumentedAttribute)
 
-    def create_managed_attribute(self, obj, key, uselist, callable_=None, attrdict=None, typecallable=None, **kwargs):
-        """creates a new ManagedAttribute corresponding to the given attribute key on the 
-        given object instance, and installs it in the attribute dictionary attached to the object."""
-        if callable_ is not None:
-            prop = self.create_callable(obj, key, callable_, uselist=uselist, typecallable=typecallable, **kwargs)
-        elif not uselist:
-            prop = self.create_scalar(obj, key, **kwargs)
-        else:
-            prop = self.create_list(obj, key, None, typecallable=typecallable, **kwargs)
-        if attrdict is None:
-            attrdict = self.attribute_history(obj)
-        attrdict[key] = prop
-        return prop
-    
-    # deprecated
-    create_history=create_managed_attribute
+    def init_instance_attribute(self, obj, key, uselist, callable_=None, **kwargs):
+        """initializes an attribute on an instance to either a blank value, cancelling
+        out any class- or instance-level callables that were present, or if a callable
+        is supplied sets the callable to be invoked when the attribute is next accessed."""
+        getattr(obj.__class__, key).set_callable(obj, callable_)
+        
+    def create_prop(self, class_, key, uselist, callable_, typecallable, **kwargs):
+        """creates a scalar property object, defaulting to InstrumentedAttribute, which 
+        will communicate change events back to this AttributeManager."""
+        return InstrumentedAttribute(self, key, uselist, callable_, typecallable, **kwargs)
     
     def register_attribute(self, class_, key, uselist, callable_=None, **kwargs):
-        """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 (i.e. a lazy loader).  The definition for this attribute is 
-        wrapped up into a callable which is then stored in the corresponding
-        SmartProperty object attached to the class.  When instances of the class 
-        are created and the attribute first referenced, the callable is invoked with
-        the new object instance as an argument to create the new ManagedAttribute.  
-        Extra keyword arguments can be sent which
-        will be passed along to newly created ManagedAttribute."""
-        if not hasattr(class_, '_attribute_manager'):
-            class_._attribute_manager = self
+        """registers an attribute at the class level to be instrumented for all instances
+        of the class."""
+        if not hasattr(class_, '_state'):
+            def _get_state(self):
+                try:
+                    return self.__sa_attr_state
+                except AttributeError:
+                    self.__sa_attr_state = {}
+                    return self.__sa_attr_state
+            class_._state = property(_get_state)
+            
         typecallable = getattr(class_, key, None)
-        # TODO: look at existing properties on the class, and adapt them to the SmartProperty
-        if isinstance(typecallable, SmartProperty):
+        if isinstance(typecallable, InstrumentedAttribute):
             typecallable = None
         setattr(class_, key, self.create_prop(class_, key, uselist, callable_, typecallable=typecallable, **kwargs))
 
index 7d3b341ab0757b115b3cce94b921f3de48202480..145ec5e9b6bd84a2a750e6b7236d9e9c4257913a 100644 (file)
@@ -10,6 +10,7 @@ together to allow processing of scalar- and list-based dependencies at flush tim
 
 from sync import ONETOMANY,MANYTOONE,MANYTOMANY
 from sqlalchemy import sql, util
+import session as sessionlib
 
 def create_dependency_processor(key, syncrules, cascade, secondary=None, association=None, is_backref=False, post_update=False):
     types = {
@@ -78,7 +79,7 @@ class DependencyProcessor(object):
     def get_object_dependencies(self, obj, uowcommit, passive = True):
         """returns the list of objects that are dependent on the given object, as according to the relationship
         this dependency processor represents"""
-        return uowcommit.uow.attributes.get_history(obj, self.key, passive = passive)
+        return sessionlib.attribute_manager.get_history(obj, self.key, passive = passive)
 
 
 class OneToManyDP(DependencyProcessor):
index 0fbcdb8e1761cf82d05bb996a4b0053b23210abc..90db7f83e5103f3fcf352e56c89b7d0ca786299c 100644 (file)
@@ -436,7 +436,7 @@ class Mapper(object):
         if not self.non_primary and (mapper_registry.has_key(self.class_key) and not self.is_primary):
              raise exceptions.ArgumentError("Class '%s' already has a primary mapper defined.  Use is_primary=True to assign a new primary mapper to the class, or use non_primary=True to create a non primary Mapper" % self.class_)
 
-        sessionlib.global_attributes.reset_class_managed(self.class_)
+        sessionlib.attribute_manager.reset_class_managed(self.class_)
     
         oldinit = self.class_.__init__
         def init(self, *args, **kwargs):
@@ -447,7 +447,7 @@ class Mapper(object):
 
                 # this gets the AttributeManager to do some pre-initialization,
                 # in order to save on KeyErrors later on
-                sessionlib.global_attributes.init_attr(self)
+                sessionlib.attribute_manager.init_attr(self)
 
             if kwargs.has_key('_sa_session'):
                 session = kwargs.pop('_sa_session')
@@ -595,13 +595,15 @@ class Mapper(object):
         offset = kwargs.get('offset', None)
         populate_existing = kwargs.get('populate_existing', False)
         
-        result = util.HistoryArraySet()
+        result = util.UniqueAppender([])
         if mappers:
             otherresults = []
             for m in mappers:
-                otherresults.append(util.HistoryArraySet())
+                otherresults.append(util.UniqueAppender([]))
                 
         imap = {}
+        scratch = {}
+        imap['_scratch'] = scratch
         while True:
             row = cursor.fetchone()
             if row is None:
@@ -614,11 +616,14 @@ class Mapper(object):
                 
         # store new stuff in the identity map
         for value in imap.values():
+            if value is scratch:
+                continue
             session._register_clean(value)
-
+            
         if mappers:
-            result = [result] + otherresults
-        return result
+            return [result.data] + [o.data for o in otherresults]
+        else:
+            return result.data
         
     def identity_key(self, primary_key):
         """returns the instance key for the given identity value.  this is a global tracking object used by the Session, and is usually available off a mapped object as instance._instance_key."""
@@ -952,7 +957,7 @@ class Mapper(object):
                     prop.execute(session, instance, row, identitykey, imap, True)
             if self.extension.append_result(self, session, row, imap, result, instance, isnew, populate_existing=populate_existing) is EXT_PASS:
                 if result is not None:
-                    result.append_nohistory(instance)
+                    result.append(instance)
             return instance
                     
         # look in result-local identitymap for it.
@@ -981,7 +986,7 @@ class Mapper(object):
             self.populate_instance(session, instance, row, identitykey, imap, isnew)
         if self.extension.append_result(self, session, row, imap, result, instance, isnew, populate_existing=populate_existing) is EXT_PASS:
             if result is not None:
-                result.append_nohistory(instance)
+                result.append(instance)
         return instance
 
     def _create_instance(self, session):
@@ -990,7 +995,7 @@ class Mapper(object):
         
         # this gets the AttributeManager to do some pre-initialization,
         # in order to save on KeyErrors later on
-        sessionlib.global_attributes.init_attr(obj)
+        sessionlib.attribute_manager.init_attr(obj)
 
         return obj
 
@@ -1207,8 +1212,7 @@ class MapperExtension(object):
         current result set
         
         result - an instance of util.HistoryArraySet(), which may be an attribute on an
-        object if this is a related object load (lazy or eager).  use result.append_nohistory(value)
-        to append objects to this list.
+        object if this is a related object load (lazy or eager).  
         
         instance - the object instance to be appended to the result
         
index 23cdc78f11aea2d11e5c0636e68eea47a9eb1b1e..843bcf8173f2bacb644e168014a9851023352492 100644 (file)
@@ -28,7 +28,7 @@ class ColumnProperty(mapper.MapperProperty):
     def setattr(self, object, value):
         setattr(object, self.key, value)
     def get_history(self, obj, passive=False):
-        return sessionlib.global_attributes.get_history(obj, self.key, passive=passive)
+        return sessionlib.attribute_manager.get_history(obj, self.key, passive=passive)
     def copy(self):
         return ColumnProperty(*self.columns)
     def setup(self, key, statement, eagertable=None, **options):
@@ -41,10 +41,12 @@ class ColumnProperty(mapper.MapperProperty):
         # establish a SmartProperty property manager on the object for this key
         if parent._is_primary_mapper():
             #print "regiser col on class %s key %s" % (parent.class_.__name__, key)
-            sessionlib.global_attributes.register_attribute(parent.class_, key, uselist = False)
+            sessionlib.attribute_manager.register_attribute(parent.class_, key, uselist = False)
     def execute(self, session, instance, row, identitykey, imap, isnew):
         if isnew:
             #print "POPULATING OBJ", instance.__class__.__name__, "COL", self.columns[0]._label, "WITH DATA", row[self.columns[0]], "ROW IS A", row.__class__.__name__, "COL ID", id(self.columns[0])
+            # set a scalar object instance directly on the object, 
+            # bypassing SmartProperty event handlers.
             instance.__dict__[self.key] = row[self.columns[0]]
     def __repr__(self):
         return "ColumnProperty(%s)" % repr([str(c) for c in self.columns])
@@ -61,7 +63,7 @@ class DeferredColumnProperty(ColumnProperty):
         # establish a SmartProperty property manager on the object for this key, 
         # containing a callable to load in the attribute
         if self.is_primary():
-            sessionlib.global_attributes.register_attribute(parent.class_, key, uselist=False, callable_=lambda i:self.setup_loader(i))
+            sessionlib.attribute_manager.register_attribute(parent.class_, key, uselist=False, callable_=lambda i:self.setup_loader(i))
     def setup_loader(self, instance):
         if not self.localparent.is_assigned(instance):
             return mapper.object_mapper(instance).props[self.key].setup_loader(instance)
@@ -88,8 +90,10 @@ class DeferredColumnProperty(ColumnProperty):
                     for prop in groupcols:
                         if prop is self:
                             continue
+                        # set a scalar object instance directly on the object, 
+                        # bypassing SmartProperty event handlers.
                         instance.__dict__[prop.key] = row[prop.columns[0]]
-                        sessionlib.global_attributes.create_history(instance, prop.key, uselist=False)
+                        sessionlib.attribute_manager.init_instance_attribute(instance, prop.key, uselist=False)
                     return row[self.columns[0]]    
                 else:
                     return connection.scalar(sql.select([self.columns[0]], clause, use_labels=True),None)
@@ -101,9 +105,9 @@ class DeferredColumnProperty(ColumnProperty):
     def execute(self, session, instance, row, identitykey, imap, isnew):
         if isnew:
             if not self.is_primary():
-                sessionlib.global_attributes.create_history(instance, self.key, False, callable_=self.setup_loader(instance))
+                sessionlib.attribute_manager.init_instance_attribute(instance, self.key, False, callable_=self.setup_loader(instance))
             else:
-                sessionlib.global_attributes.reset_history(instance, self.key)
+                sessionlib.attribute_manager.reset_instance_attribute(instance, self.key)
 
 mapper.ColumnProperty = ColumnProperty
 
@@ -150,7 +154,7 @@ class PropertyLoader(mapper.MapperProperty):
     def cascade_iterator(self, type, object, recursive):
         if not type in self.cascade:
             return
-        childlist = sessionlib.global_attributes.get_history(object, self.key, passive=True)
+        childlist = sessionlib.attribute_manager.get_history(object, self.key, passive=True)
         
         mapper = self.mapper.primary_mapper()
         for c in childlist.added_items() + childlist.deleted_items() + childlist.unchanged_items():
@@ -163,9 +167,9 @@ class PropertyLoader(mapper.MapperProperty):
     def cascade_callable(self, type, object, callable_, recursive):
         if not type in self.cascade:
             return
-        childlist = sessionlib.global_attributes.get_history(object, self.key, passive=True)
+        
         mapper = self.mapper.primary_mapper()
-        for c in childlist.added_items() + childlist.deleted_items() + childlist.unchanged_items():
+        for c in sessionlib.attribute_manager.get_as_list(object, self.key, passive=True):
             if c is not None and c not in recursive:
                 recursive.add(c)
                 callable_(c, mapper.entity_name)
@@ -241,16 +245,16 @@ class PropertyLoader(mapper.MapperProperty):
 
             if self.backref is not None:
                 self.backref.compile(self)
-        elif not sessionlib.global_attributes.is_class_managed(parent.class_, key):
+        elif not sessionlib.attribute_manager.is_class_managed(parent.class_, key):
             raise exceptions.ArgumentError("Attempting to assign a new relation '%s' to a non-primary mapper on class '%s'.  New relations can only be added to the primary mapper, i.e. the very first mapper created for class '%s' " % (key, parent.class_.__name__, parent.class_.__name__))
 
         self.do_init_subclass(key, parent)
 
     def _register_attribute(self, class_, callable_=None):
-        sessionlib.global_attributes.register_attribute(class_, self.key, uselist = self.uselist, extension=self.attributeext, cascade=self.cascade,  trackparent=True, callable_=callable_)
+        sessionlib.attribute_manager.register_attribute(class_, self.key, uselist = self.uselist, extension=self.attributeext, cascade=self.cascade,  trackparent=True, callable_=callable_)
 
-    def _create_history(self, instance, callable_=None):
-        return sessionlib.global_attributes.create_history(instance, self.key, self.uselist, cascade=self.cascade,  trackparent=True, callable_=callable_)
+    def _init_instance_attribute(self, instance, callable_=None):
+        return sessionlib.attribute_manager.init_instance_attribute(instance, self.key, self.uselist, cascade=self.cascade,  trackparent=True, callable_=callable_)
         
     def _set_class_attribute(self, class_, key):
         """sets attribute behavior on our target class."""
@@ -298,7 +302,7 @@ class PropertyLoader(mapper.MapperProperty):
         if self.is_primary():
             return
         #print "PLAIN PROPLOADER EXEC NON-PRIAMRY", repr(id(self)), repr(self.mapper.class_), self.key
-        self._create_history(instance)
+        self._init_instance_attribute(instance)
 
     def register_dependencies(self, uowcommit):
         self._dependency_processor.register_dependencies(uowcommit)
@@ -388,15 +392,15 @@ class LazyLoader(PropertyLoader):
             if not self.is_primary():
                 #print "EXEC NON-PRIAMRY", repr(self.mapper.class_), self.key
                 # we are not the primary manager for this attribute on this class - set up a per-instance lazyloader,
-                # which will override the class-level behavior
-                self._create_history(instance, callable_=self.setup_loader(instance))
+                # which will override the clareset_instance_attributess-level behavior
+                self._init_instance_attribute(instance, callable_=self.setup_loader(instance))
             else:
                 #print "EXEC PRIMARY", repr(self.mapper.class_), self.key
                 # we are the primary manager for this attribute on this class - reset its per-instance attribute state, 
                 # so that the class-level lazy loader is executed when next referenced on this instance.
                 # this usually is not needed unless the constructor of the object referenced the attribute before we got 
                 # to load data into it.
-                sessionlib.global_attributes.reset_history(instance, self.key)
+                sessionlib.attribute_manager.reset_instance_attribute(instance, self.key)
  
 def create_lazy_clause(table, primaryjoin, secondaryjoin, foreignkey):
     binds = {}
@@ -559,24 +563,29 @@ class EagerLoader(LazyLoader):
             LazyLoader.execute(self, session, instance, row, identitykey, imap, isnew)
             return
                 
-        if isnew:
-            # new row loaded from the database.  initialize a blank container on the instance.
-            # this will override any per-class lazyloading type of stuff.
-            h = self._create_history(instance)
             
         if not self.uselist:
             if isnew:
-                h.setattr_clean(self.mapper._instance(session, decorated_row, imap, None))
+                # set a scalar object instance directly on the parent object, 
+                # bypassing SmartProperty event handlers.
+                instance.__dict__[self.key] = self.mapper._instance(session, decorated_row, imap, None)
             else:
                 # call _instance on the row, even though the object has been created,
                 # so that we further descend into properties
                 self.mapper._instance(session, decorated_row, imap, None)
                 
             return
-        elif isnew:
-            result_list = h
         else:
-            result_list = getattr(instance, self.key)
+            if isnew:
+                # call the SmartProperty's initialize() method to create a new, blank list
+                l = getattr(instance.__class__, self.key).initialize(instance)
+                
+                # create an appender object which will add set-like semantics to the list
+                appender = util.UniqueAppender(l.data)
+                
+                # store it in the "scratch" area, which is local to this load operation.
+                imap['_scratch'][(instance, self.key)] = appender
+            result_list = imap['_scratch'][(instance, self.key)]
         self.mapper._instance(session, decorated_row, imap, result_list)
 
     def _create_decorator_row(self):
index 1ba6d1a35e23408adfef71245a9ac2a0cd4f20e6..7c86fafb506fbb43020ae37fa2d6fb08aeeec452 100644 (file)
@@ -339,7 +339,7 @@ class Session(object):
             return
         if not hasattr(object, '_instance_key'):
             raise exceptions.InvalidRequestError("Instance '%s' is not persisted" % repr(object))
-        if global_attributes.is_modified(object):
+        if attribute_manager.is_modified(object):
             self._register_dirty(object)
         else:
             self._register_clean(object)
@@ -425,7 +425,7 @@ def class_mapper(class_, **kwargs):
 
 # this is the AttributeManager instance used to provide attribute behavior on objects.
 # to all the "global variable police" out there:  its a stateless object.
-global_attributes = unitofwork.global_attributes
+attribute_manager = unitofwork.attribute_manager
 
 # this dictionary maps the hash key of a Session to the Session itself, and 
 # acts as a Registry with which to locate Sessions.  this is to enable
index 5c222edee2efe9f47739f58b8e68b4effe816044..dcaf750dea78d350cb5a0ab7be70070896a13132 100644 (file)
@@ -28,41 +28,28 @@ import sets
 # with the "echo_uow=True" keyword argument.
 LOG = False
 
-class UOWProperty(attributes.SmartProperty):
-    """overrides SmartProperty to provide ORM-specific accessors"""
-    def __init__(self, class_, *args, **kwargs):
-        super(UOWProperty, self).__init__(*args, **kwargs)
+class UOWEventHandler(attributes.AttributeExtension):
+    """an event handler added to all class attributes which handles session operations."""
+    def __init__(self, key, class_, cascade=None):
+        self.key = key
         self.class_ = class_
-    property = property(lambda s:class_mapper(s.class_).props[s.key], doc="returns the MapperProperty object associated with this property")
-
-                
-class UOWListElement(attributes.ListAttribute):
-    """overrides ListElement to provide unit-of-work "dirty" hooks when list attributes are modified,
-    plus specialzed append() method."""
-    def __init__(self, obj, key, data=None, cascade=None, **kwargs):
-        attributes.ListAttribute.__init__(self, obj, key, data=data, **kwargs)
         self.cascade = cascade
-    def do_value_changed(self, obj, key, item, listval, isdelete):
+    def append(self, event, obj, item):
         sess = object_session(obj)
         if sess is not None:
             sess._register_changed(obj)
-            if self.cascade is not None and not isdelete and self.cascade.save_update and item not in sess:
+            if self.cascade is not None and self.cascade.save_update and item not in sess:
                 mapper = object_mapper(obj)
                 prop = mapper.props[self.key]
                 ename = prop.mapper.entity_name
                 sess.save_or_update(item, entity_name=ename)
-    def append(self, item, _mapper_nohistory = False):
-        if _mapper_nohistory:
-            self.append_nohistory(item)
-        else:
-            attributes.ListAttribute.append(self, item)
-
-class UOWScalarElement(attributes.ScalarAttribute):
-    def __init__(self, obj, key, cascade=None, **kwargs):
-        attributes.ScalarAttribute.__init__(self, obj, key, **kwargs)
-        self.cascade=cascade
-    def do_value_changed(self, oldvalue, newvalue):
-        obj = self.obj
+
+    def delete(self, event, obj, item):
+        sess = object_session(obj)
+        if sess is not None:
+            sess._register_changed(obj)
+
+    def set(self, event, obj, newvalue, oldvalue):
         sess = object_session(obj)
         if sess is not None:
             sess._register_changed(obj)
@@ -71,21 +58,24 @@ class UOWScalarElement(attributes.ScalarAttribute):
                 prop = mapper.props[self.key]
                 ename = prop.mapper.entity_name
                 sess.save_or_update(newvalue, entity_name=ename)
+
+class UOWProperty(attributes.InstrumentedAttribute):
+    """overrides InstrumentedAttribute to provide an extra AttributeExtension to all managed attributes
+    as well as the 'property' property."""
+    def __init__(self, manager, class_, key, uselist, callable_, typecallable, cascade=None, extension=None, **kwargs):
+        extension = util.to_list(extension or [])
+        extension.insert(0, UOWEventHandler(key, class_, cascade=cascade))
+        super(UOWProperty, self).__init__(manager, key, uselist, callable_, typecallable, extension=extension,**kwargs)
+        self.class_ = class_
+        
+    property = property(lambda s:class_mapper(s.class_).props[s.key], doc="returns the MapperProperty object associated with this property")
             
 class UOWAttributeManager(attributes.AttributeManager):
-    """overrides AttributeManager to provide unit-of-work "dirty" hooks when scalar attribues are modified, plus factory methods for UOWProperrty/UOWListElement."""
-    def __init__(self):
-        attributes.AttributeManager.__init__(self)
+    """overrides AttributeManager to provide the UOWProperty instance for all InstrumentedAttributes."""
+    def create_prop(self, class_, key, uselist, callable_, typecallable, **kwargs):
+        return UOWProperty(self, class_, key, uselist, callable_, typecallable, **kwargs)
 
-    def create_prop(self, class_, key, uselist, callable_, **kwargs):
-        return UOWProperty(class_, self, key, uselist, callable_, **kwargs)
 
-    def create_scalar(self, obj, key, **kwargs):
-        return UOWScalarElement(obj, key, **kwargs)
-        
-    def create_list(self, obj, key, list_, **kwargs):
-        return UOWListElement(obj, key, list_, **kwargs)
-        
 class UnitOfWork(object):
     """main UOW object which stores lists of dirty/new/deleted objects.  provides top-level "flush" functionality as well as the transaction boundaries with the SQLEngine(s) involved in a write operation."""
     def __init__(self, identity_map=None):
@@ -94,7 +84,6 @@ class UnitOfWork(object):
         else:
             self.identity_map = weakref.WeakValueDictionary()
             
-        self.attributes = global_attributes
         self.new = util.OrderedSet()
         self.dirty = util.Set()
         
@@ -112,19 +101,17 @@ class UnitOfWork(object):
         self.identity_map[key] = obj
 
     def refresh(self, sess, obj):
-        self.rollback_object(obj)
         sess.query(obj.__class__)._get(obj._instance_key, reload=True)
 
     def expire(self, sess, obj):
-        self.rollback_object(obj)
         def exp():
             sess.query(obj.__class__)._get(obj._instance_key, reload=True)
-        global_attributes.trigger_history(obj, exp)
+        attribute_manager.trigger_history(obj, exp)
     
     def is_expired(self, obj, unexpire=False):
-        ret = global_attributes.has_trigger(obj)
+        ret = attribute_manager.has_trigger(obj)
         if ret and unexpire:
-            global_attributes.untrigger_history(obj)
+            attribute_manager.untrigger_history(obj)
         return ret
             
     def has_key(self, key):
@@ -151,8 +138,6 @@ class UnitOfWork(object):
             self.new.remove(obj)
         except KeyError:
             pass
-        #self.attributes.commit(obj)
-        self.attributes.remove(obj)
 
     def _validate_obj(self, obj):
         """validates that dirty/delete/flush operations can occur upon the given object, by checking
@@ -167,10 +152,10 @@ class UnitOfWork(object):
         self.register_dirty(obj)
         
     def register_attribute(self, class_, key, uselist, **kwargs):
-        self.attributes.register_attribute(class_, key, uselist, **kwargs)
+        attribute_manager.register_attribute(class_, key, uselist, **kwargs)
 
     def register_callable(self, obj, key, func, uselist, **kwargs):
-        self.attributes.set_callable(obj, key, func, uselist, **kwargs)
+        attribute_manager.set_callable(obj, key, func, uselist, **kwargs)
     
     def register_clean(self, obj):
         try:
@@ -185,7 +170,7 @@ class UnitOfWork(object):
             mapper = object_mapper(obj)
             obj._instance_key = mapper.instance_key(obj)
         self._put(obj._instance_key, obj)
-        self.attributes.commit(obj)
+        attribute_manager.commit(obj)
         
     def register_new(self, obj):
         if hasattr(obj, '_instance_key'):
@@ -251,7 +236,7 @@ class UnitOfWork(object):
 
     def rollback_object(self, obj):
         """'rolls back' the attributes that have been changed on an object instance."""
-        self.attributes.rollback(obj)
+        attribute_manager.rollback(obj)
         try:
             self.dirty.remove(obj)
         except KeyError:
@@ -906,5 +891,5 @@ def object_mapper(obj):
 def class_mapper(class_):
     return sqlalchemy.class_mapper(class_)
 
-global_attributes = UOWAttributeManager()
+attribute_manager = UOWAttributeManager()
 
index 5f6d1796caaec7e0426dc747be35e874360682ce..0e328238563a48d2e11415fae168608a7e787f36 100644 (file)
@@ -208,188 +208,19 @@ class OrderedSet(sets.Set):
         self._data = OrderedDict()
         if iterable is not None: 
           self._update(iterable)
-        
-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 
-    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
-        self.records = OrderedDict()
-        if data is not None:
-            self.data = data
-            for item in data:
-                # add items without triggering any change events
-                # *ASSUME* the list is unique already.  might want to change this.
-                self.records[item] = None
-        else:
-            self.data = []
-        self.readonly=readonly
-    def __iter__(self):
-        return iter(self.data)
-    def __getattr__(self, attr):
-        """proxies unknown HistoryArraySet methods and attributes to the underlying
-        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 item in self.data:
-            self.records[item] = False
-            self.do_value_deleted(item)
-            
-        # switch array
-        self.data = data
-
-        # TODO: fix this up, remove items from array while iterating
-        for i in range(0, len(self.data)):
-            if not self.__setrecord(self.data[i], False):
-                del self.data[i]
-                i -= 1
-        for item in self.data:
-            self.do_value_appended(item)
-    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)
-    def do_value_appended(self, value):
-        pass
-    def do_value_deleted(self, value):
-        pass
-    def __setrecord(self, item, dochanged=True):
-        try:
-            val = self.records[item]
-            if val is True or val is None:
-                return False
-            else:
-                self.records[item] = None
-                if dochanged:
-                    self.do_value_appended(item)
-                return True
-        except KeyError:
-            self.records[item] = True
-            if dochanged:
-                self.do_value_appended(item)
-            return True
-    def __delrecord(self, item, dochanged=True):
-        try:
-            val = self.records[item]
-            if val is None:
-                self.records[item] = False
-                if dochanged:
-                    self.do_value_deleted(item)
-                return True
-            elif val is True:
-                del self.records[item]
-                if dochanged:
-                    self.do_value_deleted(item)
-                return True
-            return False
-        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:
-                del self.records[key]
-            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():
-            if status is False or status is None:
-                list.append(key)
-        self._clear_data()
-        self.records = {}
-        for l in list:
-            self.append_nohistory(l)
-    def clear(self):
-        """clears the list and removes all history."""
-        self._clear_data()
-        self.records = {}
-    def _clear_data(self):
-        if isinstance(self.data, dict):
-            self.data.clear()
-        else:
-            self.data[:] = []
-    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)
-    def has_item(self, item):
-        return self.records.has_key(item) and self.records[item] is not False
-    def __setitem__(self, i, item): 
-        if self.__setrecord(item):
-            self.data[i] = item
-    def __delitem__(self, i):
-        self.__delrecord(self.data[i])
-        del self.data[i]
-    def __setslice__(self, i, j, other):
-        print "HAS SETSLICE"
-        i = max(i, 0); j = max(j, 0)
-        if isinstance(other, UserList.UserList):
-            l = other.data
-        elif isinstance(other, type(self.data)):
-            l = other
-        else:
-            l = list(other)
-        [self.__delrecord(x) for x in self.data[i:]]
-        g = [a for a in l if self.__setrecord(a)]
-        self.data[i:] = g
-    def __delslice__(self, i, j):
-        i = max(i, 0); j = max(j, 0)
-        for a in self.data[i:j]:
-            self.__delrecord(a)
-        del self.data[i:j]
-    def append(self, item): 
-        if self.__setrecord(item):
-            self.data.append(item)
-    def insert(self, i, item): 
-        if self.__setrecord(item):
-            self.data.insert(i, item)
-    def pop(self, i=-1):
-        item = self.data[i]
-        if self.__delrecord(item):
-            return self.data.pop(i)
-    def remove(self, item): 
-        if self.__delrecord(item):
-            self.data.remove(item)
-    def extend(self, item_list):
-        for item in item_list:
-            self.append(item)            
-    def __add__(self, other):
-        raise NotImplementedError()
-    def __radd__(self, other):
-        raise NotImplementedError()
-    def __iadd__(self, other):
-        raise NotImplementedError()
 
+class UniqueAppender(object):
+    def __init__(self, data):
+        self.data = data
+        if hasattr(data, 'append'):
+            self._data_appender = data.append
+        elif hasattr(data, 'add'):
+            self._data_appender = data.add
+        self.set = Set()
+    def append(self, item):
+        if item not in self.set:
+            self.set.add(item)
+            self._data_appender(item)
         
 class ScopedRegistry(object):
     """a Registry that can store one or multiple instances of a single class 
index 292ce5528c0fee96f8dfc730892b1f69b901f961..baf218f06f2fb3d03d77eea0777cb708ea501736 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@ use_setuptools()
 from setuptools import setup, find_packages
 
 setup(name = "SQLAlchemy",
-    version = "0.2.2",
+    version = "0.2.3",
     description = "Database Abstraction Library",
     author = "Mike Bayer",
     author_email = "mike_mp@zzzcomputing.com",
index 382e9863efcbd885145646c1061ed03e7874d0f6..4b8bfd39ae774a4dd9858cc7c6e77bd1c27460bd 100644 (file)
@@ -45,7 +45,9 @@ class AttributesTest(PersistTest):
         manager.register_attribute(MyTest, 'email_address', uselist = False)
         x = MyTest()
         x.user_id=7
-        pickle.dumps(x)
+        s = pickle.dumps(x)
+        x2 = pickle.loads(s)
+        assert s == pickle.dumps(x2)
 
     def testlist(self):
         class User(object):pass
@@ -86,7 +88,7 @@ class AttributesTest(PersistTest):
         print repr(u.__dict__)
         print repr(u.addresses[0].__dict__)
         self.assert_(u.user_id == 7 and u.user_name == 'john' and u.addresses[0].email_address == 'lala@123.com')
-        self.assert_(len(u.addresses.unchanged_items()) == 1)
+        self.assert_(len(manager.get_history(u, 'addresses').unchanged_items()) == 1)
 
     def testbackref(self):
         class Student(object):pass
@@ -98,6 +100,8 @@ class AttributesTest(PersistTest):
         s = Student()
         c = Course()
         s.courses.append(c)
+        print c.students
+        print [s]
         self.assert_(c.students == [s])
         s.courses.remove(c)
         self.assert_(c.students == [])
@@ -106,6 +110,11 @@ class AttributesTest(PersistTest):
         c.students = [s1, s2, s3]
         self.assert_(s2.courses == [c])
         self.assert_(s1.courses == [c])
+        print "--------------------------------"
+        print s1
+        print s1.courses
+        print c
+        print c.students
         s1.courses.remove(c)
         self.assert_(c.students == [s2,s3])
         
@@ -129,6 +138,10 @@ class AttributesTest(PersistTest):
         p4.blog = b
         self.assert_(b.posts == [p1, p2, p4])
         
+        p4.blog = b
+        p4.blog = b
+        self.assert_(b.posts == [p1, p2, p4])
+
 
         class Port(object):pass
         class Jack(object):pass
@@ -151,10 +164,13 @@ class AttributesTest(PersistTest):
         manager = attributes.AttributeManager()
         
         def func1():
+            print "func1"
             return "this is the foo attr"
         def func2():
+            print "func2"
             return "this is the bar attr"
         def func3():
+            print "func3"
             return "this is the shared attr"
         manager.register_attribute(Foo, 'element', uselist=False, callable_=lambda o:func1)
         manager.register_attribute(Foo, 'element2', uselist=False, callable_=lambda o:func3)
@@ -167,28 +183,56 @@ class AttributesTest(PersistTest):
         assert x.element2 == 'this is the shared attr'
         assert y.element2 == 'this is the shared attr'
 
+    def testlazyhistory(self):
+        """tests that history functions work with lazy-loading attributes"""
+        class Foo(object):pass
+        class Bar(object):
+            def __init__(self, id):
+                self.id = id
+            def __repr__(self):
+                return "Bar: id %d" % self.id
+                
+        manager = attributes.AttributeManager()
+
+        def func1():
+            return "this is func 1"
+        def func2():
+            return [Bar(1), Bar(2), Bar(3)]
+
+        manager.register_attribute(Foo, 'col1', uselist=False, callable_=lambda o:func1)
+        manager.register_attribute(Foo, 'col2', uselist=True, callable_=lambda o:func2)
+        manager.register_attribute(Bar, 'id', uselist=False)
+
+        x = Foo()
+        manager.commit(x)
+        x.col2.append(Bar(4))
+        h = manager.get_history(x, 'col2')
+        print h.added_items()
+        print h.unchanged_items()
+
+        
     def testparenttrack(self):    
         class Foo(object):pass
         class Bar(object):pass
-
+        
         manager = attributes.AttributeManager()
-
+        
         manager.register_attribute(Foo, 'element', uselist=False, trackparent=True)
         manager.register_attribute(Bar, 'element', uselist=False, trackparent=True)
-
+        
         f1 = Foo()
         f2 = Foo()
         b1 = Bar()
         b2 = Bar()
-
+        
         f1.element = b1
         b2.element = f2
-
+        
         assert manager.get_history(f1, 'element').hasparent(b1)
         assert not manager.get_history(f1, 'element').hasparent(b2)
         assert not manager.get_history(f1, 'element').hasparent(f2)
         assert manager.get_history(b2, 'element').hasparent(f2)
-
+        
         b2.element = None
         assert not manager.get_history(b2, 'element').hasparent(f2)
         
index 84beee739f510524281362b066900fe1f581efbe..eb8ac5a8965d3715b2e0539b35262d32d8ab8957 100644 (file)
@@ -14,7 +14,7 @@ class Issue(BaseObject):
 
 class Location(BaseObject):
     def __repr__(self):
-        return "%s(%s, %s)" % (self.__class__.__name__, str(self.issue_id), repr(str(self._name.name)))
+        return "%s(%s, %s)" % (self.__class__.__name__, str(getattr(self, 'issue_id', None)), repr(str(self._name.name)))
 
     def _get_name(self):
         return self._name
@@ -187,7 +187,6 @@ class InheritTest(testbase.AssertMixin):
         page2 = MagazinePage(magazine=magazine,page_no=2)
         page3 = ClassifiedPage(magazine=magazine,page_no=3)
         session.save(pub)
-        print [x for x in session]
         
         session.flush()
         print [x for x in session]
index e5055ef583275236cf604d7d41fd5282677e53ea..577903d472dd770eb1fa69d5ae099aa0302d3c88 100644 (file)
@@ -263,7 +263,6 @@ class M2MTest2(testbase.AssertMixin):
         c3 = Course('Course3')
         s1.courses.append(c1)
         s1.courses.append(c2)
-        c1.students.append(s1)
         c3.students.append(s1)
         self.assert_(len(s1.courses) == 3)
         self.assert_(len(c1.students) == 1)
index de38629ece4d1c7dd5feb5284df57815f169778c..96b092d89d9ccb0a3be055cb214f833e1ab1b2af 100644 (file)
@@ -165,7 +165,7 @@ class MapperTest(MapperSuperTest):
         users.update(users.c.user_id==7, values=dict(user_name='jack')).execute()
         s.expire(u)
         # object isnt refreshed yet, using dict to bypass trigger
-        self.assert_(u.__dict__['user_name'] != 'jack')
+        self.assert_(u.__dict__.get('user_name') != 'jack')
         # do a select
         s.query(User).select()
         # test that it refreshed
@@ -662,7 +662,7 @@ class LazyTest(MapperSuperTest):
         openorders = alias(orders, 'openorders')
         closedorders = alias(orders, 'closedorders')
         m = mapper(User, users, properties = dict(
-            addresses = relation(mapper(Address, addresses), lazy = False),
+            addresses = relation(mapper(Address, addresses), lazy = True),
             open_orders = relation(mapper(Order, openorders, entity_name='open'), primaryjoin = and_(openorders.c.isopen == 1, users.c.user_id==openorders.c.user_id), lazy = True),
             closed_orders = relation(mapper(Order, closedorders,entity_name='closed'), primaryjoin = and_(closedorders.c.isopen == 0, users.c.user_id==closedorders.c.user_id), lazy = True)
         ))
@@ -708,6 +708,7 @@ class LazyTest(MapperSuperTest):
 
 class EagerTest(MapperSuperTest):
     def testbasic(self):
+        testbase.db.echo="debug"
         """tests a basic one-to-many eager load"""
         m = mapper(Address, addresses)
         
index 4dc9624a98dc2e884ac7817835601f6736ada722..d267520f3d25e1369d38247a07d776b6bb3bb384 100644 (file)
@@ -58,12 +58,13 @@ class HistoryTest(SessionTest):
 
         self.echo(repr(u.addresses))
         ctx.current.uow.rollback_object(u)
-        data = [User,
-            {'user_name' : None,
-             'addresses' : (Address, [])
-            },
-        ]
-        self.assert_result([u], data[0], *data[1:])
+        
+        # depending on the setting in the get() method of InstrumentedAttribute in attributes.py, 
+        # username is either None or is a non-present attribute.
+        assert u.user_name is None
+        #assert not hasattr(u, 'user_name')
+        
+        assert u.addresses == []
 
     def testbackref(self):
         s = create_session()
@@ -618,7 +619,26 @@ class SaveTest(SessionTest):
         ctx.current.clear()
         u = m.get(id)
         assert u.user_name == 'imnew'
+    
+    def testhistoryget(self):
+        """tests that the history properly lazy-fetches data when it wasnt otherwise loaded"""
+        mapper(User, users, properties={
+            'addresses':relation(Address, cascade="all, delete-orphan")
+        })
+        mapper(Address, addresses)
         
+        u = User()
+        u.addresses.append(Address())
+        u.addresses.append(Address())
+        ctx.current.flush()
+        ctx.current.clear()
+        u = ctx.current.query(User).get(u.user_id)
+        ctx.current.delete(u)
+        ctx.current.flush()
+        assert users.count().scalar() == 0
+        assert addresses.count().scalar() == 0
+        
+            
     def testm2mmultitable(self):
         # many-to-many join on an association table
         j = join(users, userkeywords, 
@@ -821,6 +841,7 @@ class SaveTest(SessionTest):
         u[0].addresses[0].email_address='hi'
         
         # insure that upon commit, the new mapper with the address relation is used
+        ctx.current.echo_uow=True
         self.assert_sql(db, lambda: ctx.current.flush(), 
                 [
                     (
index d36746968b8d9aae5f4ca746cbca0f735c1b21c7..2efb650aaa49be53ecea706ae34960924fe0ce09 100644 (file)
@@ -43,22 +43,23 @@ class LoadTest(AssertMixin):
         class Item(object):pass
             
         m = mapper(Item, items)
-        
+        sess = create_session()
+        query = sess.query(Item)
         for x in range (1,NUM/100):
             # this is not needed with cpython which clears non-circular refs immediately
             #gc.collect()
-            l = m.select(items.c.item_id.between(x*100 - 100, x*100 - 1))
+            l = query.select(items.c.item_id.between(x*100 - 100, x*100 - 1))
             assert len(l) == 100
             print "loaded ", len(l), " items "
             # modifying each object will insure that the objects get placed in the "dirty" list
             # and will hang around until expunged
-            for a in l:
-                a.value = 'changed...'
-            assert len(objectstore.get_session().dirty) == len(l)
-            assert len(objectstore.get_session().identity_map) == len(l)
-            assert len(attributes.managed_attributes) == len(l)
-            print len(objectstore.get_session().dirty)
-            print len(objectstore.get_session().identity_map)
+            #for a in l:
+            #    a.value = 'changed...'
+            #assert len(objectstore.get_session().dirty) == len(l)
+            #assert len(objectstore.get_session().identity_map) == len(l)
+            #assert len(attributes.managed_attributes) == len(l)
+            #print len(objectstore.get_session().dirty)
+            #print len(objectstore.get_session().identity_map)
             #objectstore.expunge(*l)
 
 if __name__ == "__main__":