From 84fdccc0cb1819a58a08d8e78caf3e02f12fc372 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 15 Jun 2006 15:53:00 +0000 Subject: [PATCH] merged attributes rewrite --- CHANGES | 8 +- doc/build/content/document_base.myt | 2 +- lib/sqlalchemy/attributes.py | 983 +++++++++++++++------------- lib/sqlalchemy/orm/dependency.py | 3 +- lib/sqlalchemy/orm/mapper.py | 28 +- lib/sqlalchemy/orm/properties.py | 59 +- lib/sqlalchemy/orm/session.py | 4 +- lib/sqlalchemy/orm/unitofwork.py | 85 +-- lib/sqlalchemy/util.py | 193 +----- setup.py | 2 +- test/base/attributes.py | 60 +- test/orm/inheritance3.py | 3 +- test/orm/manytomany.py | 1 - test/orm/mapper.py | 5 +- test/orm/objectstore.py | 33 +- test/perf/massload.py | 19 +- 16 files changed, 746 insertions(+), 742 deletions(-) diff --git a/CHANGES b/CHANGES index 5a5f24e70a..8c17e72dd9 100644 --- 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 diff --git a/doc/build/content/document_base.myt b/doc/build/content/document_base.myt index f88a836ffc..21f9f87803 100644 --- a/doc/build/content/document_base.myt +++ b/doc/build/content/document_base.myt @@ -24,7 +24,7 @@ onepage='documentation' index='index' title='SQLAlchemy 0.2 Documentation' - version = '0.2.2' + version = '0.2.3' <%method title> diff --git a/lib/sqlalchemy/attributes.py b/lib/sqlalchemy/attributes.py index 07b395fee0..6444cd1abc 100644 --- a/lib/sqlalchemy/attributes.py +++ b/lib/sqlalchemy/attributes.py @@ -4,529 +4,632 @@ # 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)) diff --git a/lib/sqlalchemy/orm/dependency.py b/lib/sqlalchemy/orm/dependency.py index 7d3b341ab0..145ec5e9b6 100644 --- a/lib/sqlalchemy/orm/dependency.py +++ b/lib/sqlalchemy/orm/dependency.py @@ -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): diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 0fbcdb8e17..90db7f83e5 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -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 diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 23cdc78f11..843bcf8173 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -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): diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 1ba6d1a35e..7c86fafb50 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -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 diff --git a/lib/sqlalchemy/orm/unitofwork.py b/lib/sqlalchemy/orm/unitofwork.py index 5c222edee2..dcaf750dea 100644 --- a/lib/sqlalchemy/orm/unitofwork.py +++ b/lib/sqlalchemy/orm/unitofwork.py @@ -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() diff --git a/lib/sqlalchemy/util.py b/lib/sqlalchemy/util.py index 5f6d1796ca..0e32823856 100644 --- a/lib/sqlalchemy/util.py +++ b/lib/sqlalchemy/util.py @@ -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 diff --git a/setup.py b/setup.py index 292ce5528c..baf218f06f 100644 --- 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", diff --git a/test/base/attributes.py b/test/base/attributes.py index 382e9863ef..4b8bfd39ae 100644 --- a/test/base/attributes.py +++ b/test/base/attributes.py @@ -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) diff --git a/test/orm/inheritance3.py b/test/orm/inheritance3.py index 84beee739f..eb8ac5a896 100644 --- a/test/orm/inheritance3.py +++ b/test/orm/inheritance3.py @@ -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] diff --git a/test/orm/manytomany.py b/test/orm/manytomany.py index e5055ef583..577903d472 100644 --- a/test/orm/manytomany.py +++ b/test/orm/manytomany.py @@ -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) diff --git a/test/orm/mapper.py b/test/orm/mapper.py index de38629ece..96b092d89d 100644 --- a/test/orm/mapper.py +++ b/test/orm/mapper.py @@ -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) diff --git a/test/orm/objectstore.py b/test/orm/objectstore.py index 4dc9624a98..d267520f3d 100644 --- a/test/orm/objectstore.py +++ b/test/orm/objectstore.py @@ -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(), [ ( diff --git a/test/perf/massload.py b/test/perf/massload.py index d36746968b..2efb650aaa 100644 --- a/test/perf/massload.py +++ b/test/perf/massload.py @@ -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__": -- 2.47.2