diff options
Diffstat (limited to 'lib/sqlalchemy/orm/attributes.py')
-rw-r--r-- | lib/sqlalchemy/orm/attributes.py | 836 |
1 files changed, 363 insertions, 473 deletions
diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py index 9f8a04db8..47ff26085 100644 --- a/lib/sqlalchemy/orm/attributes.py +++ b/lib/sqlalchemy/orm/attributes.py @@ -4,37 +4,67 @@ # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -from sqlalchemy import util -from sqlalchemy.orm import util as orm_util -from sqlalchemy import logging, exceptions import weakref -class InstrumentedAttribute(object): - """A property object that instruments attribute access on object instances. +from sqlalchemy import util +from sqlalchemy.orm import util as orm_util, interfaces, collections +from sqlalchemy.orm.mapper import class_mapper +from sqlalchemy import logging, exceptions - All methods correspond to a single attribute on a particular - class. - """ - PASSIVE_NORESULT = object() +PASSIVE_NORESULT = object() +ATTR_WAS_SET = object() - def __init__(self, manager, key, uselist, callable_, typecallable, trackparent=False, extension=None, copy_function=None, compare_function=None, mutable_scalars=False, **kwargs): +class InstrumentedAttribute(interfaces.PropComparator): + """attribute access for instrumented classes.""" + + def __init__(self, class_, manager, key, callable_, trackparent=False, extension=None, compare_function=None, mutable_scalars=False, comparator=None, **kwargs): + """Construct an InstrumentedAttribute. + + class_ + the class to be instrumented. + + manager + AttributeManager managing this class + + key + string name of the attribute + + callable_ + optional function which generates a callable based on a parent + instance, which produces the "default" values for a scalar or + collection attribute when it's first accessed, if not present already. + + trackparent + if True, attempt to track if an instance has a parent attached to it + via this attribute + + extension + an AttributeExtension object which will receive + set/delete/append/remove/etc. events + + compare_function + a function that compares two values which are normally assignable to this + attribute + + mutable_scalars + if True, the values which are normally assignable to this attribute can mutate, + and need to be compared against a copy of their original contents in order to + detect changes on the parent instance + + comparator + a sql.Comparator to which class-level compare/math events will be sent + + """ + + self.class_ = class_ self.manager = manager self.key = key - self.uselist = uselist self.callable_ = callable_ - self.typecallable= typecallable self.trackparent = trackparent self.mutable_scalars = mutable_scalars - if copy_function is None: - if uselist: - self.copy = lambda x:[y for y in x] - else: - # scalar values are assumed to be immutable unless a copy function - # is passed - self.copy = lambda x:x - else: - self.copy = lambda x:copy_function(x) + self.comparator = comparator + self.copy = None if compare_function is None: self.is_equal = lambda x,y: x == y else: @@ -42,7 +72,7 @@ class InstrumentedAttribute(object): self.extensions = util.to_list(extension or []) def __set__(self, obj, value): - self.set(None, obj, value) + self.set(obj, value, None) def __delete__(self, obj): self.delete(None, obj) @@ -52,17 +82,18 @@ class InstrumentedAttribute(object): return self return self.get(obj) - def check_mutable_modified(self, obj): - if self.mutable_scalars: - h = self.get_history(obj, passive=True) - if h is not None and h.is_modified(): - obj._state['modified'] = True - return True - else: - return False - else: - return False + def clause_element(self): + return self.comparator.clause_element() + + def expression_element(self): + return self.comparator.expression_element() + + def operate(self, op, other, **kwargs): + return op(self.comparator, other, **kwargs) + def reverse_operate(self, op, other, **kwargs): + return op(other, self.comparator, **kwargs) + def hasparent(self, item, optimistic=False): """Return the boolean value of a `hasparent` flag attached to the given item. @@ -98,8 +129,8 @@ class InstrumentedAttribute(object): # get the current state. this may trigger a lazy load if # passive is False. - current = self.get(obj, passive=passive, raiseerr=False) - if current is InstrumentedAttribute.PASSIVE_NORESULT: + current = self.get(obj, passive=passive) + if current is PASSIVE_NORESULT: return None return AttributeHistory(self, obj, current, passive=passive) @@ -123,6 +154,14 @@ class InstrumentedAttribute(object): else: obj._state[('callable', self)] = callable_ + def _get_callable(self, obj): + if ('callable', self) in obj._state: + return obj._state[('callable', self)] + elif self.callable_ is not None: + return self.callable_(obj) + else: + return None + def reset(self, obj): """Remove any per-instance callable functions corresponding to this ``InstrumentedAttribute``'s attribute from the given @@ -148,43 +187,21 @@ class InstrumentedAttribute(object): 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 None - - def _blank_list(self): - if self.typecallable is not None: - return self.typecallable() - else: - return [] + def check_mutable_modified(self, obj): + return False def initialize(self, obj): - """Initialize this attribute on the given object instance. + """Initialize this attribute on the given object instance with an empty value.""" - If this is a list-based attribute, a new, blank list will be - created. if a scalar attribute, the value will be initialized - to None. - """ - - 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 + obj.__dict__[self.key] = None + return None - def get(self, obj, passive=False, raiseerr=True): + def get(self, obj, passive=False): """Retrieve 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. + resulting value will be set as the new value for this attribute. """ try: @@ -193,441 +210,301 @@ class InstrumentedAttribute(object): state = obj._state # if an instance-wide "trigger" was set, call that # and start again - if state.has_key('trigger'): + if 'trigger' in state: 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 InstrumentedAttribute.PASSIVE_NORESULT - self.logger.debug("Executing lazy callable on %s.%s" % (orm_util.instance_str(obj), self.key)) - values = callable_() - l = InstrumentedList(self, obj, values, init=False) - - # if a callable was executed, then its part of the "committed state" - # if any, so commit the newly loaded data - orig = state.get('original', None) - if orig is not None: - orig.commit_attribute(self, obj, l) - + return self.get(obj, passive=passive) + + callable_ = self._get_callable(obj) + if callable_ is not None: + if passive: + return PASSIVE_NORESULT + self.logger.debug("Executing lazy callable on %s.%s" % + (orm_util.instance_str(obj), self.key)) + value = callable_() + if value is not ATTR_WAS_SET: + return self.set_committed_value(obj, value) else: - # note that we arent raising AttributeErrors, just creating a new - # blank list and setting it. - # this might be a good thing to be changeable by options. - l = InstrumentedList(self, obj, self._blank_list(), init=False) - obj.__dict__[self.key] = l - return l - else: - callable_ = self._get_callable(obj) - if callable_ is not None: - if passive: - return InstrumentedAttribute.PASSIVE_NORESULT - self.logger.debug("Executing lazy callable on %s.%s" % (orm_util.instance_str(obj), self.key)) - value = callable_() - obj.__dict__[self.key] = value - - # if a callable was executed, then its part of the "committed state" - # if any, so commit the newly loaded data - orig = state.get('original', None) - if orig is not None: - orig.commit_attribute(self, obj) - return value - else: - # note that we arent raising AttributeErrors, just returning None. - # this might be a good thing to be changeable by options. - return None - - def set(self, event, obj, value): - """Set 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 an instance-wide "trigger" was set, call that - if state.has_key('trigger'): - trig = state['trigger'] - del state['trigger'] - trig() - if self.uselist: - value = InstrumentedList(self, obj, value) - old = self.get(obj) - 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) + return obj.__dict__[self.key] else: - # mark all the old elements as detached from the parent - old.list_replaced() + # Return a new, empty value + return self.initialize(obj) - def delete(self, event, obj): - """Delete a value from the given object. + def append(self, obj, value, initiator): + self.set(obj, value, initiator) - `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: - if not self.uselist and (self.trackparent or len(self.extensions)): - old = self.get(obj) - del obj.__dict__[self.key] - except KeyError: - # TODO: raise this? not consistent with get() ? - raise AttributeError(self.key) - obj._state['modified'] = True - if not self.uselist: - 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 append(self, event, obj, value): - """Append 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. - """ + def remove(self, obj, value, initiator): + self.set(obj, None, initiator) - 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): - """Remove 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. + def set(self, obj, value, initiator): + raise NotImplementedError() - `event` is the ``InstrumentedAttribute`` that initiated the - ``remove()`` operation and is used to control the depth of a - circular remove operation. + def set_committed_value(self, obj, value): + """set an attribute value on the given instance and 'commit' it. + + this indicates that the given value is the "persisted" value, + and history will be logged only if a newly set value is not + equal to this value. + + this is typically used by deferred/lazy attribute loaders + to set object attributes after the initial load. """ - if self.uselist: - if event is not self: - self.get(obj).remove_with_event(value, event) - else: - self.set(event, obj, None) + state = obj._state + orig = state.get('original', None) + if orig is not None: + orig.commit_attribute(self, obj, value) + # remove per-instance callable, if any + state.pop(('callable', self), None) + obj.__dict__[self.key] = value + return value - def append_event(self, event, obj, value): - """Called by ``InstrumentedList`` when an item is appended.""" + def set_raw_value(self, obj, value): + obj.__dict__[self.key] = value + return value + def fire_append_event(self, obj, value, initiator): obj._state['modified'] = True if self.trackparent and value is not None: self.sethasparent(value, True) for ext in self.extensions: - ext.append(event or self, obj, value) - - def remove_event(self, event, obj, value): - """Called by ``InstrumentedList`` when an item is removed.""" + ext.append(obj, value, initiator or self) + def fire_remove_event(self, obj, value, initiator): obj._state['modified'] = True if self.trackparent and value is not None: self.sethasparent(value, False) for ext in self.extensions: - ext.delete(event or self, obj, value) + ext.remove(obj, value, initiator or self) + + def fire_replace_event(self, obj, value, previous, initiator): + obj._state['modified'] = True + if self.trackparent: + if value is not None: + self.sethasparent(value, True) + if previous is not None: + self.sethasparent(previous, False) + for ext in self.extensions: + ext.set(obj, value, previous, initiator or self) + + property = property(lambda s: class_mapper(s.class_).get_property(s.key), + doc="the MapperProperty object associated with this attribute") InstrumentedAttribute.logger = logging.class_logger(InstrumentedAttribute) + +class InstrumentedScalarAttribute(InstrumentedAttribute): + """represents a scalar-holding InstrumentedAttribute.""" -class InstrumentedList(object): - """Instrument 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``. - - 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. - - Please see ticket #213 for information on the future of this - class, where it will be broken out into more collection-specific - subtypes. - """ + def __init__(self, class_, manager, key, callable_, trackparent=False, extension=None, copy_function=None, compare_function=None, mutable_scalars=False, **kwargs): + super(InstrumentedScalarAttribute, self).__init__(class_, manager, key, + callable_, trackparent=trackparent, extension=extension, + compare_function=compare_function, **kwargs) + self.mutable_scalars = mutable_scalars - 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 - - # adapt to lists or sets - # TODO: make three subclasses of InstrumentedList that come off from a - # metaclass, based on the type of data sent in - if attr.typecallable is not None: - self.data = attr.typecallable() - else: - self.data = data or attr._blank_list() - - if isinstance(self.data, list): - self._data_appender = self.data.append - self._clear_data = self._clear_list - elif isinstance(self.data, util.Set): - self._data_appender = self.data.add - self._clear_data = self._clear_set - elif isinstance(self.data, dict): - if hasattr(self.data, 'append'): - self._data_appender = self.data.append - else: - raise exceptions.ArgumentError("Dictionary collection class '%s' must implement an append() method" % type(self.data).__name__) - self._clear_data = self._clear_dict - else: - if hasattr(self.data, 'append'): - self._data_appender = self.data.append - elif hasattr(self.data, 'add'): - self._data_appender = self.data.add - else: - raise exceptions.ArgumentError("Collection class '%s' is not of type 'list', 'set', or 'dict' and has no append() or add() method" % type(self.data).__name__) + if copy_function is None: + copy_function = self.__copy + self.copy = copy_function - if hasattr(self.data, 'clear'): - self._clear_data = self._clear_set - else: - raise exceptions.ArgumentError("Collection class '%s' is not of type 'list', 'set', or 'dict' and has no clear() method" % type(self.data).__name__) - - if data is not None and data is not self.data: - for elem in data: - self._data_appender(elem) - + def __copy(self, item): + # scalar values are assumed to be immutable unless a copy function + # is passed + return item - if init: - for x in self.data: - self.__setrecord(x) + def __delete__(self, obj): + old = self.get(obj) + del obj.__dict__[self.key] + self.fire_remove_event(obj, old, self) - def list_replaced(self): - """Fire off delete event handlers for each item in the list - but doesnt affect the original data list. - """ + def check_mutable_modified(self, obj): + if self.mutable_scalars: + h = self.get_history(obj, passive=True) + if h is not None and h.is_modified(): + obj._state['modified'] = True + return True + else: + return False + else: + return False - [self.__delrecord(x) for x in self.data] + def set(self, obj, value, initiator): + """Set a value on the given object. - def clear(self): - """Clear all items in this InstrumentedList and fires off - delete event handlers for each item. + `initiator` is the ``InstrumentedAttribute`` that initiated the + ``set()` operation and is used to control the depth of a circular + setter operation. """ - self._clear_data() - - def _clear_dict(self): - [self.__delrecord(x) for x in self.data.values()] - self.data.clear() - - def _clear_set(self): - [self.__delrecord(x) for x in self.data] - self.data.clear() - - def _clear_list(self): - self[:] = [] - - def __getstate__(self): - """Implemented to allow pickling, since `__obj` is a weakref, - also the ``InstrumentedAttribute`` has callables attached to - it. - """ + if initiator is self: + return - return {'key':self.key, 'obj':self.obj, 'data':self.data} + state = obj._state + # if an instance-wide "trigger" was set, call that + if 'trigger' in state: + trig = state['trigger'] + del state['trigger'] + trig() - def __setstate__(self, d): - """Implemented to allow pickling, since `__obj` is a weakref, - also the ``InstrumentedAttribute`` has callables attached to it. - """ + old = self.get(obj) + obj.__dict__[self.key] = value + self.fire_replace_event(obj, value, old, initiator) - self.key = d['key'] - self.__obj = weakref.ref(d['obj']) - self.data = d['data'] - self.attr = getattr(d['obj'].__class__, self.key) + type = property(lambda self: self.property.columns[0].type) - obj = property(lambda s:s.__obj()) + +class InstrumentedCollectionAttribute(InstrumentedAttribute): + """A collection-holding attribute that instruments changes in membership. - def unchanged_items(self): - """Deprecated.""" + InstrumentedCollectionAttribute holds an arbitrary, user-specified + container object (defaulting to a list) and brokers access to the + CollectionAdapter, a "view" onto that object that presents consistent + bag semantics to the orm layer independent of the user data implementation. + """ + + def __init__(self, class_, manager, key, callable_, typecallable=None, trackparent=False, extension=None, copy_function=None, compare_function=None, **kwargs): + super(InstrumentedCollectionAttribute, self).__init__(class_, manager, + key, callable_, trackparent=trackparent, extension=extension, + compare_function=compare_function, **kwargs) - return self.attr.get_history(self.obj).unchanged_items + if copy_function is None: + copy_function = self.__copy + self.copy = copy_function - def added_items(self): - """Deprecated.""" + if typecallable is None: + typecallable = list + self.collection_factory = \ + collections._prepare_instrumentation(typecallable) + self.collection_interface = \ + util.duck_type_collection(self.collection_factory()) - return self.attr.get_history(self.obj).added_items + def __copy(self, item): + return [y for y in list(collections.collection_adapter(item))] - def deleted_items(self): - """Deprecated.""" + def __set__(self, obj, value): + """Replace the current collection with a new one.""" - return self.attr.get_history(self.obj).deleted_items + setting_type = util.duck_type_collection(value) - def __iter__(self): - return iter(self.data) + if value is None or setting_type != self.collection_interface: + raise exceptions.ArgumentError( + "Incompatible collection type on assignment: %s is not %s-like" % + (type(value).__name__, self.collection_interface.__name__)) - def __repr__(self): - return repr(self.data) + if hasattr(value, '_sa_adapter'): + self.set(obj, list(getattr(value, '_sa_adapter')), None) + elif setting_type == dict: + self.set(obj, value.values(), None) + else: + self.set(obj, value, None) - def __getattr__(self, attr): - """Proxy unknown methods and attributes to the underlying - data array. This allows custom list classes to be used. - """ + def __delete__(self, obj): + if self.key not in obj.__dict__: + return - return getattr(self.data, attr) + obj._state['modified'] = True - def __setrecord(self, item, event=None): - self.attr.append_event(event, self.obj, item) - return True + collection = self._get_collection(obj) + collection.clear_with_event() + del obj.__dict__[self.key] - def __delrecord(self, item, event=None): - self.attr.remove_event(event, self.obj, item) - return True + def initialize(self, obj): + """Initialize this attribute on the given object instance with an empty collection.""" - def append_with_event(self, item, event): - self.__setrecord(item, event) - self._data_appender(item) + _, user_data = self._build_collection(obj) + obj.__dict__[self.key] = user_data + return user_data - def append_without_event(self, item): - self._data_appender(item) + def append(self, obj, value, initiator): + if initiator is self: + return + collection = self._get_collection(obj) + collection.append_with_event(value, initiator) - def remove_with_event(self, item, event): - self.__delrecord(item, event) - self.data.remove(item) + def remove(self, obj, value, initiator): + if initiator is self: + return + collection = self._get_collection(obj) + collection.remove_with_event(value, initiator) - def append(self, item, _mapper_nohistory=False): - """Fire off dependent events, and appends the given item to the underlying list. + def set(self, obj, value, initiator): + """Set a value on the given object. - `_mapper_nohistory` is a backwards compatibility hack; call - ``append_without_event`` instead. + `initiator` is the ``InstrumentedAttribute`` that initiated the + ``set()` operation and is used to control the depth of a circular + setter operation. """ - if _mapper_nohistory: - self.append_without_event(item) - else: - self.__setrecord(item) - self._data_appender(item) - - 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 __lt__(self, other): return self.data < self.__cast(other) - - def __le__(self, other): return self.data <= self.__cast(other) + if initiator is self: + return - def __eq__(self, other): return self.data == self.__cast(other) + state = obj._state + # if an instance-wide "trigger" was set, call that + if 'trigger' in state: + trig = state['trigger'] + del state['trigger'] + trig() - def __ne__(self, other): return self.data != self.__cast(other) + old = self.get(obj) + old_collection = self._get_collection(obj, old) - def __gt__(self, other): return self.data > self.__cast(other) + new_collection, user_data = self._build_collection(obj) + self._load_collection(obj, value or [], emit_events=True, + collection=new_collection) - def __ge__(self, other): return self.data >= self.__cast(other) + obj.__dict__[self.key] = user_data + state['modified'] = True - def __cast(self, other): - if isinstance(other, InstrumentedList): return other.data - else: return other + # mark all the old elements as detached from the parent + if old_collection: + old_collection.clear_with_event() + old_collection.unlink(old) - def __cmp__(self, other): - return cmp(self.data, self.__cast(other)) + def set_committed_value(self, obj, value): + """Set an attribute value on the given instance and 'commit' it.""" + + state = obj._state + orig = state.get('original', None) - def __contains__(self, item): return item in self.data + collection, user_data = self._build_collection(obj) + self._load_collection(obj, value or [], emit_events=False, + collection=collection) + value = user_data - def __len__(self): + if orig is not None: + orig.commit_attribute(self, obj, value) + # remove per-instance callable, if any + state.pop(('callable', self), None) + obj.__dict__[self.key] = value + return value + + def _build_collection(self, obj): + user_data = self.collection_factory() + collection = collections.CollectionAdapter(self, obj, user_data) + return collection, user_data + + def _load_collection(self, obj, values, emit_events=True, collection=None): + collection = collection or self._get_collection(obj) + if values is None: + return + elif emit_events: + for item in values: + collection.append_with_event(item) + else: + for item in values: + collection.append_without_event(item) + + def _get_collection(self, obj, user_data=None): + if user_data is None: + user_data = self.get(obj) try: - return len(self.data) - except TypeError: - return len(list(self.data)) - - def __setslice__(self, i, j, other): - [self.__delrecord(x) for x in self.data[i:j]] - g = [a for a in list(other) if self.__setrecord(a)] - self.data[i:j] = g - - def __delslice__(self, i, j): - 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 discard(self, item): - if item in self.data: - 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 `append`, `delete`, and `set` - event handlers to be attached to an object property. - """ - - def append(self, event, obj, child): - pass - - def delete(self, event, obj, child): - pass + return getattr(user_data, '_sa_adapter') + except AttributeError: + collections.CollectionAdapter(self, obj, user_data) + return getattr(user_data, '_sa_adapter') - def set(self, event, obj, child, oldchild): - pass -class GenericBackrefExtension(AttributeExtension): +class GenericBackrefExtension(interfaces.AttributeExtension): """An extension which synchronizes a two-way relationship. A typical two-way relationship is a parent object containing a @@ -639,19 +516,19 @@ class GenericBackrefExtension(AttributeExtension): def __init__(self, key): self.key = key - def set(self, event, obj, child, oldchild): + def set(self, obj, child, oldchild, initiator): if oldchild is child: return if oldchild is not None: - getattr(oldchild.__class__, self.key).remove(event, oldchild, obj) + getattr(oldchild.__class__, self.key).remove(oldchild, obj, initiator) if child is not None: - getattr(child.__class__, self.key).append(event, child, obj) + getattr(child.__class__, self.key).append(child, obj, initiator) - def append(self, event, obj, child): - getattr(child.__class__, self.key).append(event, child, obj) + def append(self, obj, child, initiator): + getattr(child.__class__, self.key).append(child, obj, initiator) - def delete(self, event, obj, child): - getattr(child.__class__, self.key).remove(event, child, obj) + def remove(self, obj, child, initiator): + getattr(child.__class__, self.key).remove(child, obj, initiator) class CommittedState(object): """Store the original state of an object when the ``commit()` @@ -673,7 +550,7 @@ class CommittedState(object): """ if value is CommittedState.NO_VALUE: - if obj.__dict__.has_key(attr.key): + if attr.key in obj.__dict__: value = obj.__dict__[attr.key] if value is not CommittedState.NO_VALUE: self.data[attr.key] = attr.copy(value) @@ -690,10 +567,13 @@ class CommittedState(object): 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: + if not isinstance(attr, InstrumentedCollectionAttribute): obj.__dict__[attr.key] = self.data[attr.key] + else: + collection = attr._get_collection(obj) + collection.clear_without_event() + for item in self.data[attr.key]: + collection.append_without_event(item) else: del obj.__dict__[attr.key] @@ -718,17 +598,15 @@ class AttributeHistory(object): else: original = None - if attr.uselist: + if isinstance(attr, InstrumentedCollectionAttribute): 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: + collection = attr._get_collection(obj, current) + for a in collection: if a in s: self._unchanged_items.append(a) else: @@ -737,6 +615,7 @@ class AttributeHistory(object): if a not in self._unchanged_items: self._deleted_items.append(a) else: + self._current = [current] if attr.is_equal(current, original): self._unchanged_items = [current] self._added_items = [] @@ -748,7 +627,6 @@ class AttributeHistory(object): else: self._deleted_items = [] self._unchanged_items = [] - #print "key", attr.key, "orig", original, "current", current, "added", self._added_items, "unchanged", self._unchanged_items, "deleted", self._deleted_items def __iter__(self): return iter(self._current) @@ -766,24 +644,13 @@ class AttributeHistory(object): return self._deleted_items def hasparent(self, obj): - """Deprecated. This should be called directly from the - appropriate ``InstrumentedAttribute`` object. + """Deprecated. This should be called directly from the appropriate ``InstrumentedAttribute`` object. """ return self.attr.hasparent(obj) class AttributeManager(object): - """Allow the instrumentation of object attributes. - - ``AttributeManager`` is stateless, but can be overridden by - subclasses to redefine some of its factory operations. Also be - aware ``AttributeManager`` will cache attributes for a given - class, allowing not to determine those for each objects (used in - ``managed_attributes()`` and - ``noninherited_managed_attributes()``). This cache is cleared for - a given class while calling ``register_attribute()``, and can be - cleared using ``clear_attribute_cache()``. - """ + """Allow the instrumentation of object attributes.""" def __init__(self): # will cache attributes, indexed by class objects @@ -827,7 +694,7 @@ class AttributeManager(object): o._state['modified'] = False def managed_attributes(self, class_): - """Return an iterator of all ``InstrumentedAttribute`` objects + """Return a list of all ``InstrumentedAttribute`` objects associated with the given class. """ @@ -878,7 +745,7 @@ class AttributeManager(object): """Return an attribute of the given name from the given object. If the attribute is a scalar, return it as a single-item list, - otherwise return the list based attribute. + otherwise return a collection based attribute. If the attribute's value is to be produced by an unexecuted callable, the callable will only be executed if the given @@ -887,10 +754,10 @@ class AttributeManager(object): attr = getattr(obj.__class__, key) x = attr.get(obj, passive=passive) - if x is InstrumentedAttribute.PASSIVE_NORESULT: + if x is PASSIVE_NORESULT: return [] - elif attr.uselist: - return x + elif isinstance(attr, InstrumentedCollectionAttribute): + return list(attr._get_collection(obj, x)) else: return [x] @@ -921,7 +788,7 @@ class AttributeManager(object): by ``trigger_history()``. """ - return obj._state.has_key('trigger') + return 'trigger' in obj._state def reset_instance_attribute(self, obj, key): """Remove any per-instance callable functions corresponding to @@ -946,10 +813,9 @@ class AttributeManager(object): """Return 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 init_instance_attribute(self, obj, key, uselist, callable_=None, **kwargs): + def init_instance_attribute(self, obj, key, callable_=None): """Initialize 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 set the @@ -964,7 +830,24 @@ class AttributeManager(object): events back to this ``AttributeManager``. """ - return InstrumentedAttribute(self, key, uselist, callable_, typecallable, **kwargs) + if uselist: + return InstrumentedCollectionAttribute(class_, self, key, + callable_, + typecallable, + **kwargs) + else: + return InstrumentedScalarAttribute(class_, self, key, callable_, + **kwargs) + + def get_attribute(self, obj_or_cls, key): + """Register an attribute at the class level to be instrumented + for all instances of the class. + """ + + if isinstance(obj_or_cls, type): + return getattr(obj_or_cls, key) + else: + return getattr(obj_or_cls.__class__, key) def register_attribute(self, class_, key, uselist, callable_=None, **kwargs): """Register an attribute at the class level to be instrumented @@ -973,10 +856,9 @@ class AttributeManager(object): # firt invalidate the cache for the given class # (will be reconstituted as needed, while getting managed attributes) - self._inherited_attribute_cache.pop(class_,None) - self._noninherited_attribute_cache.pop(class_,None) + self._inherited_attribute_cache.pop(class_, None) + self._noninherited_attribute_cache.pop(class_, None) - #print self, "register attribute", key, "for class", class_ if not hasattr(class_, '_state'): def _get_state(self): if not hasattr(self, '_sa_attr_state'): @@ -987,4 +869,12 @@ class AttributeManager(object): typecallable = kwargs.pop('typecallable', None) if isinstance(typecallable, InstrumentedAttribute): typecallable = None - setattr(class_, key, self.create_prop(class_, key, uselist, callable_, typecallable=typecallable, **kwargs)) + setattr(class_, key, self.create_prop(class_, key, uselist, callable_, + typecallable=typecallable, **kwargs)) + + def init_collection(self, instance, key): + """Initialize a collection attribute and return the collection adapter.""" + + attr = self.get_attribute(instance, key) + user_data = attr.initialize(instance) + return attr._get_collection(instance, user_data) |