You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1416 lines
50 KiB

# orm/attributes.py
12 years ago
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
13 years ago
"""Defines instrumentation for class attributes and their interaction
with instances.
This module is usually not directly visible to user applications, but
defines a large part of the ORM's interactivity.
"""
import operator
13 years ago
from operator import itemgetter
from sqlalchemy import util, event, exc as sa_exc
from sqlalchemy.orm import interfaces, collections, events, exc as orm_exc
mapperutil = util.importlater("sqlalchemy.orm", "util")
PASSIVE_NO_RESULT = util.symbol('PASSIVE_NO_RESULT')
ATTR_WAS_SET = util.symbol('ATTR_WAS_SET')
13 years ago
ATTR_EMPTY = util.symbol('ATTR_EMPTY')
NO_VALUE = util.symbol('NO_VALUE')
NEVER_SET = util.symbol('NEVER_SET')
13 years ago
PASSIVE_RETURN_NEVER_SET = util.symbol('PASSIVE_RETURN_NEVER_SET',
12 years ago
"""Symbol indicating that loader callables can be
13 years ago
fired off, but if no callable is applicable and no value is
present, the attribute should remain non-initialized.
NEVER_SET is returned in this case.
""")
PASSIVE_NO_INITIALIZE = util.symbol('PASSIVE_NO_INITIALIZE',
"""Symbol indicating that loader callables should
12 years ago
not be fired off, and a non-initialized attribute
13 years ago
should remain that way.
""")
PASSIVE_NO_FETCH = util.symbol('PASSIVE_NO_FETCH',
12 years ago
"""Symbol indicating that loader callables should not emit SQL,
13 years ago
but a value can be fetched from the current session.
12 years ago
13 years ago
Non-initialized attributes should be initialized to an empty value.
""")
PASSIVE_NO_FETCH_RELATED = util.symbol('PASSIVE_NO_FETCH_RELATED',
"""Symbol indicating that loader callables should not emit SQL for
loading a related object, but can refresh the attributes of the local
instance in order to locate a related object in the current session.
12 years ago
13 years ago
Non-initialized attributes should be initialized to an empty value.
12 years ago
13 years ago
The unit of work uses this mode to check if history is present
on many-to-one attributes with minimal SQL emitted.
""")
PASSIVE_ONLY_PERSISTENT = util.symbol('PASSIVE_ONLY_PERSISTENT',
"""Symbol indicating that loader callables should only fire off for
13 years ago
parent objects which are persistent (i.e., have a database
identity).
13 years ago
Load operations for the "previous" value of an attribute make
use of this flag during change events.
13 years ago
""")
13 years ago
PASSIVE_OFF = util.symbol('PASSIVE_OFF',
"""Symbol indicating that loader callables should be executed
normally.
13 years ago
""")
class QueryableAttribute(interfaces.PropComparator):
13 years ago
"""Base class for class-bound attributes. """
12 years ago
def __init__(self, class_, key, impl=None,
13 years ago
comparator=None, parententity=None):
self.class_ = class_
self.key = key
self.impl = impl
self.comparator = comparator
self.parententity = parententity
13 years ago
manager = manager_of_class(class_)
# manager is None in the case of AliasedClass
if manager:
12 years ago
# propagate existing event listeners from
13 years ago
# immediate superclass
for base in manager._bases:
if key in base:
self.dispatch._update(base[key].dispatch)
dispatch = event.dispatcher(events.AttributeEvents)
dispatch.dispatch_cls._active_history = False
@util.memoized_property
def _supports_population(self):
return self.impl.supports_population
def get_history(self, instance, passive=PASSIVE_OFF):
return self.impl.get_history(instance_state(instance),
13 years ago
instance_dict(instance), passive)
def __selectable__(self):
# TODO: conditionally attach this method based on clause_element ?
return self
def __clause_element__(self):
return self.comparator.__clause_element__()
def label(self, name):
return self.__clause_element__().label(name)
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, state, optimistic=False):
13 years ago
return self.impl.hasparent(state, optimistic=optimistic) is not False
def __getattr__(self, key):
try:
return getattr(self.comparator, key)
except AttributeError:
raise AttributeError(
'Neither %r object nor %r object has an attribute %r' % (
12 years ago
type(self).__name__,
type(self.comparator).__name__,
key)
)
def __str__(self):
13 years ago
return "%s.%s" % (self.class_.__name__, self.key)
13 years ago
@util.memoized_property
def property(self):
return self.comparator.property
class InstrumentedAttribute(QueryableAttribute):
13 years ago
"""Class bound instrumented attribute which adds descriptor methods."""
def __set__(self, instance, value):
12 years ago
self.impl.set(instance_state(instance),
instance_dict(instance), value, None)
def __delete__(self, instance):
self.impl.delete(instance_state(instance), instance_dict(instance))
def __get__(self, instance, owner):
if instance is None:
return self
13 years ago
dict_ = instance_dict(instance)
if self._supports_population and self.key in dict_:
return dict_[self.key]
else:
return self.impl.get(instance_state(instance),dict_)
13 years ago
def create_proxied_attribute(descriptor):
"""Create an QueryableAttribute / user descriptor hybrid.
13 years ago
Returns a new QueryableAttribute type that delegates descriptor
behavior and getattr() to the given descriptor.
"""
13 years ago
# TODO: can move this to descriptor_props if the need for this
# function is removed from ext/hybrid.py
13 years ago
class Proxy(QueryableAttribute):
"""Presents the :class:`.QueryableAttribute` interface as a
12 years ago
proxy on top of a Python descriptor / :class:`.PropComparator`
13 years ago
combination.
"""
12 years ago
def __init__(self, class_, key, descriptor, comparator,
13 years ago
adapter=None, doc=None):
self.class_ = class_
self.key = key
13 years ago
self.descriptor = descriptor
self._comparator = comparator
13 years ago
self.adapter = adapter
self.__doc__ = doc
@property
def property(self):
return self.comparator.property
@util.memoized_property
def comparator(self):
if util.callable(self._comparator):
self._comparator = self._comparator()
13 years ago
if self.adapter:
self._comparator = self._comparator.adapted(self.adapter)
return self._comparator
13 years ago
def adapted(self, adapter):
"""Proxy adapted() for the use case of AliasedClass calling adapted."""
return self.__class__(self.class_, self.key, self.descriptor,
self._comparator,
adapter)
def __get__(self, instance, owner):
if instance is None:
return self
13 years ago
else:
return self.descriptor.__get__(instance, owner)
13 years ago
def __str__(self):
return self.key
def __getattr__(self, attribute):
"""Delegate __getattr__ to the original descriptor and/or
comparator."""
try:
return getattr(descriptor, attribute)
except AttributeError:
try:
13 years ago
return getattr(self.comparator, attribute)
except AttributeError:
raise AttributeError(
'Neither %r object nor %r object has an attribute %r' % (
12 years ago
type(descriptor).__name__,
type(self.comparator).__name__,
attribute)
)
Proxy.__name__ = type(descriptor).__name__ + 'Proxy'
util.monkeypatch_proxied_specials(Proxy, type(descriptor),
name='descriptor',
from_instance=descriptor)
return Proxy
class AttributeImpl(object):
"""internal implementation for instrumented attributes."""
def __init__(self, class_, key,
13 years ago
callable_, dispatch, trackparent=False, extension=None,
12 years ago
compare_function=None, active_history=False,
parent_token=None, expire_missing=True,
**kwargs):
"""Construct an AttributeImpl.
\class_
associated 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
a single or list of AttributeExtension object(s) which will
13 years ago
receive set/delete/append/remove/etc. events. Deprecated.
The event package is now used.
compare_function
a function that compares two values which are normally
assignable to this attribute.
active_history
indicates that get_history() should always return the "old" value,
even if it means executing a lazy callable upon attribute change.
parent_token
Usually references the MapperProperty, used as a key for
the hasparent() function to identify an "owning" attribute.
12 years ago
Allows multiple AttributeImpls to all match a single
owner attribute.
expire_missing
if False, don't add an "expiry" callable to this attribute
12 years ago
during state.expire_attributes(None), if no value is present
for this key.
"""
self.class_ = class_
self.key = key
self.callable_ = callable_
13 years ago
self.dispatch = dispatch
self.trackparent = trackparent
self.parent_token = parent_token or self
if compare_function is None:
self.is_equal = operator.eq
else:
self.is_equal = compare_function
13 years ago
# TODO: pass in the manager here
# instead of doing a lookup
attr = manager_of_class(class_)[key]
for ext in util.to_list(extension or []):
ext._adapt_listener(attr, ext)
if active_history:
self.dispatch._active_history = True
self.expire_missing = expire_missing
13 years ago
def _get_active_history(self):
"""Backwards compat for impl.active_history"""
return self.dispatch._active_history
def _set_active_history(self, value):
self.dispatch._active_history = value
active_history = property(_get_active_history, _set_active_history)
def hasparent(self, state, optimistic=False):
12 years ago
"""Return the boolean value of a `hasparent` flag attached to
the given state.
The `optimistic` flag determines what the default return value
should be if no `hasparent` flag can be located.
As this function is used to determine if an instance is an
*orphan*, instances that were loaded from storage should be
assumed to not be orphans, until a True/False value for this
flag is set.
An instance attribute that is loaded by a callable function
will also not have a `hasparent` flag.
"""
13 years ago
assert self.trackparent, "This AttributeImpl is not configured to track parents."
13 years ago
return state.parents.get(id(self.parent_token), optimistic) \
is not False
def sethasparent(self, state, parent_state, value):
"""Set 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``.
"""
13 years ago
assert self.trackparent, "This AttributeImpl is not configured to track parents."
id_ = id(self.parent_token)
if value:
state.parents[id_] = parent_state
else:
if id_ in state.parents:
last_parent = state.parents[id_]
if last_parent is not False and \
last_parent.key != parent_state.key:
if last_parent.obj() is None:
raise orm_exc.StaleDataError(
"Removing state %s from parent "
"state %s along attribute '%s', "
"but the parent record "
"has gone stale, can't be sure this "
12 years ago
"is the most recent parent." %
(mapperutil.state_str(state),
13 years ago
mapperutil.state_str(parent_state),
self.key))
return
state.parents[id_] = False
def set_callable(self, state, callable_):
"""Set a callable function for this attribute on the given object.
This callable will be executed when the attribute is next
accessed, and is assumed to construct part of the instances
previously stored state. When its value or values are loaded,
they will be established as part of the instance's *committed
state*. While *trackparent* information will be assembled for
these instances, attribute-level event handlers will not be
fired.
The callable overrides the class level callable set in the
``InstrumentedAttribute`` constructor.
"""
state.callables[self.key] = callable_
def get_history(self, state, dict_, passive=PASSIVE_OFF):
raise NotImplementedError()
13 years ago
def get_all_pending(self, state, dict_):
12 years ago
"""Return a list of tuples of (state, obj)
for all objects in this attribute's current state
13 years ago
+ history.
Only applies to object-based attributes.
This is an inlining of existing functionality
12 years ago
which roughly corresponds to:
13 years ago
get_state_history(
12 years ago
state,
key,
13 years ago
passive=PASSIVE_NO_INITIALIZE).sum()
"""
raise NotImplementedError()
def initialize(self, state, dict_):
"""Initialize the given state's attribute with an empty value."""
dict_[self.key] = None
return None
def get(self, state, dict_, passive=PASSIVE_OFF):
"""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.
"""
13 years ago
if self.key in dict_:
return dict_[self.key]
13 years ago
else:
# if history present, don't load
key = self.key
if key not in state.committed_state or \
state.committed_state[key] is NEVER_SET:
if passive is PASSIVE_NO_INITIALIZE:
return PASSIVE_NO_RESULT
13 years ago
if key in state.callables:
callable_ = state.callables[key]
value = callable_(passive)
elif self.callable_:
value = self.callable_(state, passive)
else:
value = ATTR_EMPTY
if value is PASSIVE_NO_RESULT or value is NEVER_SET:
return value
elif value is ATTR_WAS_SET:
try:
return dict_[key]
except KeyError:
# TODO: no test coverage here.
raise KeyError(
"Deferred loader for attribute "
"%r failed to populate "
"correctly" % key)
elif value is not ATTR_EMPTY:
return self.set_committed_value(state, dict_, value)
if passive is PASSIVE_RETURN_NEVER_SET:
return NEVER_SET
else:
# Return a new, empty value
return self.initialize(state, dict_)
def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
self.set(state, dict_, value, initiator, passive=passive)
def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
12 years ago
self.set(state, dict_, None, initiator,
13 years ago
passive=passive, check_old=value)
def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
12 years ago
self.set(state, dict_, None, initiator,
13 years ago
passive=passive, check_old=value, pop=True)
12 years ago
def set(self, state, dict_, value, initiator,
13 years ago
passive=PASSIVE_OFF, check_old=None, pop=False):
raise NotImplementedError()
def get_committed_value(self, state, dict_, passive=PASSIVE_OFF):
"""return the unchanged value of this attribute"""
if self.key in state.committed_state:
13 years ago
value = state.committed_state[self.key]
if value is NO_VALUE:
return None
else:
13 years ago
return value
else:
return self.get(state, dict_, passive=passive)
def set_committed_value(self, state, dict_, value):
"""set an attribute value on the given instance and 'commit' it."""
13 years ago
dict_[self.key] = value
state.commit(dict_, [self.key])
return value
class ScalarAttributeImpl(AttributeImpl):
"""represents a scalar value-holding InstrumentedAttribute."""
accepts_scalar_loader = True
uses_objects = False
supports_population = True
def delete(self, state, dict_):
# TODO: catch key errors, convert to attributeerror?
13 years ago
if self.dispatch._active_history:
old = self.get(state, dict_, PASSIVE_RETURN_NEVER_SET)
else:
old = dict_.get(self.key, NO_VALUE)
13 years ago
if self.dispatch.remove:
self.fire_remove_event(state, dict_, old, None)
13 years ago
state.modified_event(dict_, self, old)
del dict_[self.key]
def get_history(self, state, dict_, passive=PASSIVE_OFF):
13 years ago
return History.from_scalar_attribute(
self, state, dict_.get(self.key, NO_VALUE))
12 years ago
def set(self, state, dict_, value, initiator,
13 years ago
passive=PASSIVE_OFF, check_old=None, pop=False):
if initiator and initiator.parent_token is self.parent_token:
return
13 years ago
if self.dispatch._active_history:
old = self.get(state, dict_, PASSIVE_RETURN_NEVER_SET)
else:
old = dict_.get(self.key, NO_VALUE)
13 years ago
if self.dispatch.set:
12 years ago
value = self.fire_replace_event(state, dict_,
value, old, initiator)
13 years ago
state.modified_event(dict_, self, old)
dict_[self.key] = value
def fire_replace_event(self, state, dict_, value, previous, initiator):
13 years ago
for fn in self.dispatch.set:
value = fn(state, value, previous, initiator or self)
return value
def fire_remove_event(self, state, dict_, value, initiator):
13 years ago
for fn in self.dispatch.remove:
fn(state, value, initiator or self)
@property
def type(self):
self.property.columns[0].type
class MutableScalarAttributeImpl(ScalarAttributeImpl):
"""represents a scalar value-holding InstrumentedAttribute, which can
detect changes within the value itself.
"""
uses_objects = False
supports_population = True
13 years ago
def __init__(self, class_, key, callable_, dispatch,
class_manager, copy_function=None,
compare_function=None, **kwargs):
super(ScalarAttributeImpl, self).__init__(
12 years ago
class_,
key,
13 years ago
callable_, dispatch,
12 years ago
compare_function=compare_function,
**kwargs)
class_manager.mutable_attributes.add(key)
if copy_function is None:
raise sa_exc.ArgumentError(
"MutableScalarAttributeImpl requires a copy function")
self.copy = copy_function
def get_history(self, state, dict_, passive=PASSIVE_OFF):
if not dict_:
v = state.committed_state.get(self.key, NO_VALUE)
else:
v = dict_.get(self.key, NO_VALUE)
13 years ago
return History.from_scalar_attribute(self, state, v)
def check_mutable_modified(self, state, dict_):
a, u, d = self.get_history(state, dict_)
return bool(a or d)
def get(self, state, dict_, passive=PASSIVE_OFF):
if self.key not in state.mutable_dict:
ret = ScalarAttributeImpl.get(self, state, dict_, passive=passive)
if ret is not PASSIVE_NO_RESULT:
state.mutable_dict[self.key] = ret
return ret
else:
return state.mutable_dict[self.key]
def delete(self, state, dict_):
ScalarAttributeImpl.delete(self, state, dict_)
13 years ago
state.mutable_dict.pop(self.key)
12 years ago
def set(self, state, dict_, value, initiator,
13 years ago
passive=PASSIVE_OFF, check_old=None, pop=False):
12 years ago
ScalarAttributeImpl.set(self, state, dict_, value,
13 years ago
initiator, passive, check_old=check_old, pop=pop)
state.mutable_dict[self.key] = value
class ScalarObjectAttributeImpl(ScalarAttributeImpl):
12 years ago
"""represents a scalar-holding InstrumentedAttribute,
where the target object is also instrumented.
Adds events to delete/set operations.
"""
accepts_scalar_loader = False
uses_objects = True
supports_population = True
def delete(self, state, dict_):
old = self.get(state, dict_)
self.fire_remove_event(state, dict_, old, self)
del dict_[self.key]
def get_history(self, state, dict_, passive=PASSIVE_OFF):
if self.key in dict_:
13 years ago
return History.from_object_attribute(self, state, dict_[self.key])
else:
13 years ago
if passive is PASSIVE_OFF:
passive = PASSIVE_RETURN_NEVER_SET
current = self.get(state, dict_, passive=passive)
if current is PASSIVE_NO_RESULT:
return HISTORY_BLANK
else:
13 years ago
return History.from_object_attribute(self, state, current)
def get_all_pending(self, state, dict_):
if self.key in dict_:
current = dict_[self.key]
if current is not None:
ret = [(instance_state(current), current)]
else:
12 years ago
ret = [(None, None)]
13 years ago
if self.key in state.committed_state:
original = state.committed_state[self.key]
if original not in (NEVER_SET, PASSIVE_NO_RESULT, None) and \
original is not current:
13 years ago
ret.append((instance_state(original), original))
return ret
else:
return []
12 years ago
def set(self, state, dict_, value, initiator,
13 years ago
passive=PASSIVE_OFF, check_old=None, pop=False):
"""Set a value on the given InstanceState.
`initiator` is the ``InstrumentedAttribute`` that initiated the
``set()`` operation and is used to control the depth of a circular
setter operation.
"""
if initiator and initiator.parent_token is self.parent_token:
return
13 years ago
if self.dispatch._active_history:
old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT)
else:
old = self.get(state, dict_, passive=PASSIVE_NO_FETCH)
13 years ago
if check_old is not None and \
old is not PASSIVE_NO_RESULT and \
check_old is not old:
if pop:
12 years ago
return
13 years ago
else:
raise ValueError(
"Object %s not associated with %s on attribute '%s'" % (
mapperutil.instance_str(check_old),
mapperutil.state_str(state),
self.key
))
value = self.fire_replace_event(state, dict_, value, old, initiator)
dict_[self.key] = value
def fire_remove_event(self, state, dict_, value, initiator):
if self.trackparent and value is not None:
13 years ago
self.sethasparent(instance_state(value), state, False)
13 years ago
for fn in self.dispatch.remove:
fn(state, value, initiator or self)
13 years ago
state.modified_event(dict_, self, value)
def fire_replace_event(self, state, dict_, value, previous, initiator):
if self.trackparent:
if (previous is not value and
previous is not None and
previous is not PASSIVE_NO_RESULT):
13 years ago
self.sethasparent(instance_state(previous), state, False)
13 years ago
for fn in self.dispatch.set:
value = fn(state, value, previous, initiator or self)
13 years ago
state.modified_event(dict_, self, previous)
if self.trackparent:
if value is not None:
13 years ago
self.sethasparent(instance_state(value), state, True)
return value
class CollectionAttributeImpl(AttributeImpl):
"""A collection-holding attribute that instruments changes in membership.
Only handles collections of instrumented objects.
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.
"""
accepts_scalar_loader = False
uses_objects = True
supports_population = True
13 years ago
def __init__(self, class_, key, callable_, dispatch,
typecallable=None, trackparent=False, extension=None,
copy_function=None, compare_function=None, **kwargs):
super(CollectionAttributeImpl, self).__init__(
12 years ago
class_,
key,
13 years ago
callable_, dispatch,
trackparent=trackparent,
extension=extension,
12 years ago
compare_function=compare_function,
**kwargs)
if copy_function is None:
copy_function = self.__copy
self.copy = copy_function
self.collection_factory = typecallable
def __copy(self, item):
return [y for y in list(collections.collection_adapter(item))]
def get_history(self, state, dict_, passive=PASSIVE_OFF):
current = self.get(state, dict_, passive=passive)
if current is PASSIVE_NO_RESULT:
return HISTORY_BLANK
else:
13 years ago
return History.from_collection(self, state, current)
def get_all_pending(self, state, dict_):
if self.key not in dict_:
return []
current = dict_[self.key]
current = getattr(current, '_sa_adapter')
if self.key in state.committed_state:
original = state.committed_state[self.key]
if original is not NO_VALUE:
12 years ago
current_states = [((c is not None) and
instance_state(c) or None, c)
13 years ago
for c in current]
12 years ago
original_states = [((c is not None) and
instance_state(c) or None, c)
13 years ago
for c in original]
current_set = dict(current_states)
original_set = dict(original_states)
return \
[(s, o) for s, o in current_states if s not in original_set] + \
[(s, o) for s, o in current_states if s in original_set] + \
[(s, o) for s, o in original_states if s not in current_set]
return [(instance_state(o), o) for o in current]
def fire_append_event(self, state, dict_, value, initiator):
13 years ago
for fn in self.dispatch.append:
value = fn(state, value, initiator or self)
13 years ago
state.modified_event(dict_, self, NEVER_SET, True)
if self.trackparent and value is not None:
13 years ago
self.sethasparent(instance_state(value), state, True)
return value
def fire_pre_remove_event(self, state, dict_, initiator):
13 years ago
state.modified_event(dict_, self, NEVER_SET, True)
def fire_remove_event(self, state, dict_, value, initiator):
if self.trackparent and value is not None:
13 years ago
self.sethasparent(instance_state(value), state, False)
13 years ago
for fn in self.dispatch.remove:
fn(state, value, initiator or self)
13 years ago
state.modified_event(dict_, self, NEVER_SET, True)
def delete(self, state, dict_):
if self.key not in dict_:
return
13 years ago
state.modified_event(dict_, self, NEVER_SET, True)
collection = self.get_collection(state, state.dict)
collection.clear_with_event()
# TODO: catch key errors, convert to attributeerror?
del dict_[self.key]
def initialize(self, state, dict_):
"""Initialize this attribute with an empty collection."""
_, user_data = self._initialize_collection(state)
dict_[self.key] = user_data
return user_data
def _initialize_collection(self, state):
return state.manager.initialize_collection(
self.key, state, self.collection_factory)
def append(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
if initiator and initiator.parent_token is self.parent_token:
return
collection = self.get_collection(state, dict_, passive=passive)
if collection is PASSIVE_NO_RESULT:
value = self.fire_append_event(state, dict_, value, initiator)
assert self.key not in dict_, \
"Collection was loaded during event handling."
state.get_pending(self.key).append(value)
else:
collection.append_with_event(value, initiator)
def remove(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
if initiator and initiator.parent_token is self.parent_token:
return
collection = self.get_collection(state, state.dict, passive=passive)
if collection is PASSIVE_NO_RESULT:
self.fire_remove_event(state, dict_, value, initiator)
assert self.key not in dict_, \
"Collection was loaded during event handling."
state.get_pending(self.key).remove(value)
else:
collection.remove_with_event(value, initiator)
13 years ago
def pop(self, state, dict_, value, initiator, passive=PASSIVE_OFF):
try:
# TODO: better solution here would be to add
12 years ago
# a "popper" role to collections.py to complement
13 years ago
# "remover".
self.remove(state, dict_, value, initiator, passive=passive)
except (ValueError, KeyError, IndexError):
pass
12 years ago
def set(self, state, dict_, value, initiator,
13 years ago
passive=PASSIVE_OFF, pop=False):
"""Set a value on the given object.
`initiator` is the ``InstrumentedAttribute`` that initiated the
``set()`` operation and is used to control the depth of a circular
setter operation.
"""
if initiator and initiator.parent_token is self.parent_token:
return
self._set_iterable(
state, dict_, value,
lambda adapter, i: adapter.adapt_like_to_iterable(i))
def _set_iterable(self, state, dict_, iterable, adapter=None):
"""Set a collection value from an iterable of state-bearers.
``adapter`` is an optional callable invoked with a CollectionAdapter
and the iterable. Should return an iterable of state-bearing
instances suitable for appending via a CollectionAdapter. Can be used
for, e.g., adapting an incoming dictionary into an iterator of values
rather than keys.
"""
# pulling a new collection first so that an adaptation exception does
# not trigger a lazy load of the old collection.
new_collection, user_data = self._initialize_collection(state)
if adapter:
new_values = list(adapter(new_collection, iterable))
else:
new_values = list(iterable)
old = self.get(state, dict_, passive=PASSIVE_ONLY_PERSISTENT)
if old is PASSIVE_NO_RESULT:
old = self.initialize(state, dict_)
elif old is iterable:
# ignore re-assignment of the current collection, as happens
# implicitly with in-place operators (foo.collection |= other)
return
13 years ago
# place a copy of "old" in state.committed_state
state.modified_event(dict_, self, old, True)
13 years ago
old_collection = getattr(old, '_sa_adapter')
dict_[self.key] = user_data
collections.bulk_replace(new_values, old_collection, new_collection)
old_collection.unlink(old)
def set_committed_value(self, state, dict_, value):
"""Set an attribute value on the given instance and 'commit' it."""
collection, user_data = self._initialize_collection(state)
if value:
13 years ago
collection.append_multiple_without_event(value)
state.dict[self.key] = user_data
state.commit(dict_, [self.key])
if self.key in state.pending:
# pending items exist. issue a modified event,
# add/remove new items.
13 years ago
state.modified_event(dict_, self, user_data, True)
pending = state.pending.pop(self.key)
added = pending.added_items
removed = pending.deleted_items
for item in added:
collection.append_without_event(item)
for item in removed:
collection.remove_without_event(item)
return user_data
12 years ago
def get_collection(self, state, dict_,
user_data=None, passive=PASSIVE_OFF):
"""Retrieve the CollectionAdapter associated with the given state.
Creates a new CollectionAdapter if one does not exist.
"""
if user_data is None:
user_data = self.get(state, dict_, passive=passive)
if user_data is PASSIVE_NO_RESULT:
return user_data
return getattr(user_data, '_sa_adapter')
13 years ago
def backref_listeners(attribute, key, uselist):
"""Apply listeners to synchronize a two-way relationship."""
13 years ago
# use easily recognizable names for stack traces
12 years ago
parent_token = attribute.impl.parent_token
def _acceptable_key_err(child_state, initiator):
raise ValueError(
"Object %s not associated with attribute of "
"type %s" % (mapperutil.state_str(child_state),
manager_of_class(initiator.class_)[initiator.key]))
13 years ago
def emit_backref_from_scalar_set_event(state, child, oldchild, initiator):
if oldchild is child:
return child
if oldchild is not None and oldchild is not PASSIVE_NO_RESULT:
# With lazy=None, there's no guarantee that the full collection is
# present when updating via a backref.
old_state, old_dict = instance_state(oldchild),\
instance_dict(oldchild)
13 years ago
impl = old_state.manager[key].impl
12 years ago
impl.pop(old_state,
old_dict,
state.obj(),
13 years ago
initiator, passive=PASSIVE_NO_FETCH)
if child is not None:
child_state, child_dict = instance_state(child),\
instance_dict(child)
12 years ago
child_impl = child_state.manager[key].impl
if initiator.parent_token is not parent_token and \
initiator.parent_token is not child_impl.parent_token:
_acceptable_key_err(state, initiator)
child_impl.append(
child_state,
child_dict,
state.obj(),
initiator,
passive=PASSIVE_NO_FETCH)
return child
13 years ago
def emit_backref_from_collection_append_event(state, child, initiator):
child_state, child_dict = instance_state(child), \
instance_dict(child)
12 years ago
child_impl = child_state.manager[key].impl
if initiator.parent_token is not parent_token and \
initiator.parent_token is not child_impl.parent_token:
_acceptable_key_err(state, initiator)
child_impl.append(
child_state,
child_dict,
state.obj(),
initiator,
passive=PASSIVE_NO_FETCH)
return child
13 years ago
def emit_backref_from_collection_remove_event(state, child, initiator):
if child is not None:
child_state, child_dict = instance_state(child),\
instance_dict(child)
12 years ago
child_impl = child_state.manager[key].impl
# can't think of a path that would produce an initiator
# mismatch here, as it would require an existing collection
# mismatch.
child_impl.pop(
child_state,
child_dict,
state.obj(),
initiator,
passive=PASSIVE_NO_FETCH)
13 years ago
if uselist:
12 years ago
event.listen(attribute, "append",
emit_backref_from_collection_append_event,
13 years ago
retval=True, raw=True)
else:
12 years ago
event.listen(attribute, "set",
emit_backref_from_scalar_set_event,
13 years ago
retval=True, raw=True)
# TODO: need coverage in test/orm/ of remove event
12 years ago
event.listen(attribute, "remove",
emit_backref_from_collection_remove_event,
13 years ago
retval=True, raw=True)
_NO_HISTORY = util.symbol('NO_HISTORY')
_NO_STATE_SYMBOLS = frozenset([
12 years ago
id(PASSIVE_NO_RESULT),
id(NO_VALUE),
13 years ago
id(NEVER_SET)])
class History(tuple):
"""A 3-tuple of added, unchanged and deleted values,
13 years ago
representing the changes which have occurred on an instrumented
attribute.
Each tuple member is an iterable sequence.
"""
__slots__ = ()
added = property(itemgetter(0))
"""Return the collection of items added to the attribute (the first tuple
element)."""
unchanged = property(itemgetter(1))
"""Return the collection of items that have not changed on the attribute
(the second tuple element)."""
deleted = property(itemgetter(2))
"""Return the collection of items that have been removed from the
attribute (the third tuple element)."""
def __new__(cls, added, unchanged, deleted):
return tuple.__new__(cls, (added, unchanged, deleted))
def __nonzero__(self):
return self != HISTORY_BLANK
def empty(self):
13 years ago
"""Return True if this :class:`.History` has no changes
and no existing, unchanged state.
"""
return not bool(
(self.added or self.deleted)
or self.unchanged and self.unchanged != [None]
12 years ago
)
def sum(self):
"""Return a collection of added + unchanged + deleted."""
return (self.added or []) +\
(self.unchanged or []) +\
(self.deleted or [])
def non_deleted(self):
"""Return a collection of added + unchanged."""
return (self.added or []) +\
(self.unchanged or [])
def non_added(self):
"""Return a collection of unchanged + deleted."""
return (self.unchanged or []) +\
(self.deleted or [])
def has_changes(self):
13 years ago
"""Return True if this :class:`.History` has changes."""
return bool(self.added or self.deleted)
def as_state(self):
return History(
13 years ago
[(c is not None)
and instance_state(c) or None
for c in self.added],
13 years ago
[(c is not None)
and instance_state(c) or None
for c in self.unchanged],
13 years ago
[(c is not None)
and instance_state(c) or None
for c in self.deleted],
)
@classmethod
13 years ago
def from_scalar_attribute(cls, attribute, state, current):
original = state.committed_state.get(attribute.key, _NO_HISTORY)
if original is _NO_HISTORY:
if current is NO_VALUE:
return cls((), (), ())
else:
13 years ago
return cls((), [current], ())
12 years ago
# don't let ClauseElement expressions here trip things up
13 years ago
elif attribute.is_equal(current, original) is True:
return cls((), [current], ())
else:
12 years ago
# current convention on native scalars is to not
13 years ago
# include information
# about missing previous value in "deleted", but
# we do include None, which helps in some primary
# key situations
if id(original) in _NO_STATE_SYMBOLS:
deleted = ()
else:
deleted = [original]
if current is NO_VALUE:
return cls((), (), deleted)
13 years ago
else:
return cls([current], (), deleted)
@classmethod
def from_object_attribute(cls, attribute, state, current):
original = state.committed_state.get(attribute.key, _NO_HISTORY)
if original is _NO_HISTORY:
if current is NO_VALUE or current is NEVER_SET:
return cls((), (), ())
else:
return cls((), [current], ())
13 years ago
elif current is original:
return cls((), [current], ())
else:
12 years ago
# current convention on related objects is to not
13 years ago
# include information
# about missing previous value in "deleted", and
# to also not include None - the dependency.py rules
12 years ago
# ignore the None in any case.
13 years ago
if id(original) in _NO_STATE_SYMBOLS or original is None:
deleted = ()
else:
deleted = [original]
if current is NO_VALUE or current is NEVER_SET:
return cls((), (), deleted)
else:
return cls([current], (), deleted)
13 years ago
@classmethod
def from_collection(cls, attribute, state, current):
original = state.committed_state.get(attribute.key, _NO_HISTORY)
current = getattr(current, '_sa_adapter')
if original is NO_VALUE:
return cls(list(current), (), ())
elif original is _NO_HISTORY:
return cls((), list(current), ())
else:
12 years ago
current_states = [((c is not None) and instance_state(c) or None, c)
for c in current
13 years ago
]
12 years ago
original_states = [((c is not None) and instance_state(c) or None, c)
for c in original
13 years ago
]
current_set = dict(current_states)
original_set = dict(original_states)
return cls(
[o for s, o in current_states if s not in original_set],
[o for s, o in current_states if s in original_set],
[o for s, o in original_states if s not in current_set]
)
HISTORY_BLANK = History(None, None, None)
13 years ago
def get_history(obj, key, passive=PASSIVE_OFF):
12 years ago
"""Return a :class:`.History` record for the given object
and attribute key.
:param obj: an object whose class is instrumented by the
attributes package.
:param key: string attribute name.
13 years ago
:param passive: indicates if the attribute should be
loaded from the database if not already present (:attr:`.PASSIVE_NO_FETCH`), and
if the attribute should be not initialized to a blank value otherwise
13 years ago
(:attr:`.PASSIVE_NO_INITIALIZE`). Default is :attr:`PASSIVE_OFF`.
"""
13 years ago
if passive is True:
util.warn_deprecated("Passing True for 'passive' is deprecated. "
"Use attributes.PASSIVE_NO_INITIALIZE")
passive = PASSIVE_NO_INITIALIZE
elif passive is False:
util.warn_deprecated("Passing False for 'passive' is "
"deprecated. Use attributes.PASSIVE_OFF")
passive = PASSIVE_OFF
return get_state_history(instance_state(obj), key, passive)
def get_state_history(state, key, passive=PASSIVE_OFF):
return state.get_history(key, passive)
def has_parent(cls, obj, key, optimistic=False):
"""TODO"""
manager = manager_of_class(cls)
state = instance_state(obj)
return manager.has_parent(state, key, optimistic)
def register_attribute(class_, key, **kw):
comparator = kw.pop('comparator', None)
parententity = kw.pop('parententity', None)
doc = kw.pop('doc', None)
12 years ago
desc = register_descriptor(class_, key,
comparator, parententity, doc=doc)
13 years ago
register_attribute_impl(class_, key, **kw)
return desc
def register_attribute_impl(class_, key,
12 years ago
uselist=False, callable_=None,
useobject=False, mutable_scalars=False,
13 years ago
impl_class=None, backref=None, **kw):
manager = manager_of_class(class_)
if uselist:
factory = kw.pop('typecallable', None)
typecallable = manager.instrument_collection_class(
key, factory or list)
else:
typecallable = kw.pop('typecallable', None)
13 years ago
dispatch = manager[key].dispatch
if impl_class:
13 years ago
impl = impl_class(class_, key, typecallable, dispatch, **kw)
elif uselist:
13 years ago
impl = CollectionAttributeImpl(class_, key, callable_, dispatch,
typecallable=typecallable, **kw)
elif useobject:
13 years ago
impl = ScalarObjectAttributeImpl(class_, key, callable_,
dispatch,**kw)
elif mutable_scalars:
13 years ago
impl = MutableScalarAttributeImpl(class_, key, callable_, dispatch,
class_manager=manager, **kw)
else:
13 years ago
impl = ScalarAttributeImpl(class_, key, callable_, dispatch, **kw)
manager[key].impl = impl
13 years ago
if backref:
backref_listeners(manager[key], backref, uselist)
13 years ago
manager.post_configure_attribute(key)
return manager[key]
12 years ago
def register_descriptor(class_, key, comparator=None,
13 years ago
parententity=None, doc=None):
manager = manager_of_class(class_)
13 years ago
descriptor = InstrumentedAttribute(class_, key, comparator=comparator,
parententity=parententity)
descriptor.__doc__ = doc
manager.instrument_attribute(key, descriptor)
13 years ago
return descriptor
def unregister_attribute(class_, key):
manager_of_class(class_).uninstrument_attribute(key)
def init_collection(obj, key):
"""Initialize a collection attribute and return the collection adapter.
This function is used to provide direct access to collection internals
for a previously unloaded attribute. e.g.::
collection_adapter = init_collection(someobject, 'elements')
for elem in values:
collection_adapter.append_without_event(elem)
For an easier way to do the above, see
:func:`~sqlalchemy.orm.attributes.set_committed_value`.
obj is an instrumented object instance. An InstanceState
12 years ago
is accepted directly for backwards compatibility but
this usage is deprecated.
"""
state = instance_state(obj)
dict_ = state.dict
return init_state_collection(state, dict_, key)
def init_state_collection(state, dict_, key):
"""Initialize a collection attribute and return the collection adapter."""
13 years ago
attr = state.manager[key].impl
user_data = attr.initialize(state, dict_)
return attr.get_collection(state, dict_, user_data)
def set_committed_value(instance, key, value):
"""Set the value of an attribute with no history events.
12 years ago
Cancels any previous history present. The value should be
a scalar value for scalar-holding attributes, or
an iterable for any collection-holding attribute.
This is the same underlying method used when a lazy loader
fires off and loads additional data from the database.
In particular, this method can be used by application code
which has loaded additional attributes or collections through
separate queries, which can then be attached to an instance
as though it were part of its original loaded state.
"""
state, dict_ = instance_state(instance), instance_dict(instance)
13 years ago
state.manager[key].impl.set_committed_value(state, dict_, value)
def set_attribute(instance, key, value):
"""Set the value of an attribute, firing history events.
This function may be used regardless of instrumentation
applied directly to the class, i.e. no descriptors are required.
Custom attribute management schemes will need to make usage
of this method to establish attribute state as understood
by SQLAlchemy.
"""
state, dict_ = instance_state(instance), instance_dict(instance)
13 years ago
state.manager[key].impl.set(state, dict_, value, None)
def get_attribute(instance, key):
"""Get the value of an attribute, firing any callables required.
This function may be used regardless of instrumentation
applied directly to the class, i.e. no descriptors are required.
Custom attribute management schemes will need to make usage
of this method to make usage of attribute state as understood
by SQLAlchemy.
"""
state, dict_ = instance_state(instance), instance_dict(instance)
13 years ago
return state.manager[key].impl.get(state, dict_)
def del_attribute(instance, key):
"""Delete the value of an attribute, firing history events.
This function may be used regardless of instrumentation
applied directly to the class, i.e. no descriptors are required.
Custom attribute management schemes will need to make usage
of this method to establish attribute state as understood
by SQLAlchemy.
"""
state, dict_ = instance_state(instance), instance_dict(instance)
13 years ago
state.manager[key].impl.delete(state, dict_)
13 years ago
def flag_modified(instance, key):
"""Mark an attribute on an instance as 'modified'.
12 years ago
This sets the 'modified' flag on the instance and
13 years ago
establishes an unconditional change event for the given attribute.
"""
13 years ago
state, dict_ = instance_state(instance), instance_dict(instance)
impl = state.manager[key].impl
state.modified_event(dict_, impl, NO_VALUE)