Packages update
This commit is contained in:
+709
-328
File diff suppressed because it is too large
Load Diff
+520
-945
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
# orm/collections.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 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
|
||||
@@ -108,9 +108,8 @@ import operator
|
||||
import sys
|
||||
import weakref
|
||||
|
||||
import sqlalchemy.exceptions as sa_exc
|
||||
from sqlalchemy.sql import expression
|
||||
from sqlalchemy import schema, util
|
||||
from sqlalchemy import schema, util, exc as sa_exc
|
||||
|
||||
|
||||
__all__ = ['collection', 'collection_adapter',
|
||||
@@ -123,7 +122,7 @@ __instrumentation_mutex = util.threading.Lock()
|
||||
def column_mapped_collection(mapping_spec):
|
||||
"""A dictionary-based collection type with column-based keying.
|
||||
|
||||
Returns a MappedCollection factory with a keying function generated
|
||||
Returns a :class:`.MappedCollection` factory with a keying function generated
|
||||
from mapping_spec, which may be a Column or a sequence of Columns.
|
||||
|
||||
The key value must be immutable for the lifetime of the object. You
|
||||
@@ -154,8 +153,9 @@ def column_mapped_collection(mapping_spec):
|
||||
def attribute_mapped_collection(attr_name):
|
||||
"""A dictionary-based collection type with attribute-based keying.
|
||||
|
||||
Returns a MappedCollection factory with a keying based on the
|
||||
'attr_name' attribute of entities in the collection.
|
||||
Returns a :class:`.MappedCollection` factory with a keying based on the
|
||||
'attr_name' attribute of entities in the collection, where ``attr_name``
|
||||
is the string name of the attribute.
|
||||
|
||||
The key value must be immutable for the lifetime of the object. You
|
||||
can not, for example, map on foreign key values if those key values will
|
||||
@@ -169,7 +169,7 @@ def attribute_mapped_collection(attr_name):
|
||||
def mapped_collection(keyfunc):
|
||||
"""A dictionary-based collection type with arbitrary keying.
|
||||
|
||||
Returns a MappedCollection factory with a keying function generated
|
||||
Returns a :class:`.MappedCollection` factory with a keying function generated
|
||||
from keyfunc, a callable that takes an entity and returns a key value.
|
||||
|
||||
The key value must be immutable for the lifetime of the object. You
|
||||
@@ -186,7 +186,7 @@ class collection(object):
|
||||
The decorators fall into two groups: annotations and interception recipes.
|
||||
|
||||
The annotating decorators (appender, remover, iterator,
|
||||
internally_instrumented, on_link) indicate the method's purpose and take no
|
||||
internally_instrumented, link) indicate the method's purpose and take no
|
||||
arguments. They are not written with parens::
|
||||
|
||||
@collection.appender
|
||||
@@ -201,10 +201,6 @@ class collection(object):
|
||||
@collection.removes_return()
|
||||
def popitem(self): ...
|
||||
|
||||
Decorators can be specified in long-hand for Python 2.3, or with
|
||||
the class-level dict attribute '__instrumentation__'- see the source
|
||||
for details.
|
||||
|
||||
"""
|
||||
# Bundled as a class solely for ease of use: packaging, doc strings,
|
||||
# importability.
|
||||
@@ -315,7 +311,7 @@ class collection(object):
|
||||
return fn
|
||||
|
||||
@staticmethod
|
||||
def on_link(fn):
|
||||
def link(fn):
|
||||
"""Tag the method as a the "linked to attribute" event handler.
|
||||
|
||||
This optional event handler will be called when the collection class
|
||||
@@ -325,7 +321,7 @@ class collection(object):
|
||||
that has been linked, or None if unlinking.
|
||||
|
||||
"""
|
||||
setattr(fn, '_sa_instrument_role', 'on_link')
|
||||
setattr(fn, '_sa_instrument_role', 'link')
|
||||
return fn
|
||||
|
||||
@staticmethod
|
||||
@@ -474,9 +470,12 @@ class CollectionAdapter(object):
|
||||
to the underlying Python collection, and emits add/remove events for
|
||||
entities entering or leaving the collection.
|
||||
|
||||
The ORM uses an CollectionAdapter exclusively for interaction with
|
||||
The ORM uses :class:`.CollectionAdapter` exclusively for interaction with
|
||||
entity collections.
|
||||
|
||||
The usage of getattr()/setattr() is currently to allow injection
|
||||
of custom methods, such as to unwrap Zope security proxies.
|
||||
|
||||
"""
|
||||
def __init__(self, attr, owner_state, data):
|
||||
self._key = attr.key
|
||||
@@ -559,6 +558,12 @@ class CollectionAdapter(object):
|
||||
"""Add or restore an entity to the collection, firing no events."""
|
||||
getattr(self._data(), '_sa_appender')(item, _sa_initiator=False)
|
||||
|
||||
def append_multiple_without_event(self, items):
|
||||
"""Add or restore an entity to the collection, firing no events."""
|
||||
appender = getattr(self._data(), '_sa_appender')
|
||||
for item in items:
|
||||
appender(item, _sa_initiator=False)
|
||||
|
||||
def remove_with_event(self, item, initiator=None):
|
||||
"""Remove an entity from the collection, firing mutation events."""
|
||||
getattr(self._data(), '_sa_remover')(item, _sa_initiator=initiator)
|
||||
@@ -569,13 +574,17 @@ class CollectionAdapter(object):
|
||||
|
||||
def clear_with_event(self, initiator=None):
|
||||
"""Empty the collection, firing a mutation event for each entity."""
|
||||
|
||||
remover = getattr(self._data(), '_sa_remover')
|
||||
for item in list(self):
|
||||
self.remove_with_event(item, initiator)
|
||||
remover(item, _sa_initiator=initiator)
|
||||
|
||||
def clear_without_event(self):
|
||||
"""Empty the collection, firing no events."""
|
||||
|
||||
remover = getattr(self._data(), '_sa_remover')
|
||||
for item in list(self):
|
||||
self.remove_without_event(item)
|
||||
remover(item, _sa_initiator=False)
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over entities in the collection."""
|
||||
@@ -651,14 +660,11 @@ def bulk_replace(values, existing_adapter, new_adapter):
|
||||
instances in ``existing_adapter`` not present in ``values`` will have
|
||||
remove events fired upon them.
|
||||
|
||||
values
|
||||
An iterable of collection member instances
|
||||
:param values: An iterable of collection member instances
|
||||
|
||||
existing_adapter
|
||||
A CollectionAdapter of instances to be replaced
|
||||
:param existing_adapter: A :class:`.CollectionAdapter` of instances to be replaced
|
||||
|
||||
new_adapter
|
||||
An empty CollectionAdapter to load with ``values``
|
||||
:param new_adapter: An empty :class:`.CollectionAdapter` to load with ``values``
|
||||
|
||||
|
||||
"""
|
||||
@@ -788,7 +794,7 @@ def _instrument_class(cls):
|
||||
if hasattr(method, '_sa_instrument_role'):
|
||||
role = method._sa_instrument_role
|
||||
assert role in ('appender', 'remover', 'iterator',
|
||||
'on_link', 'converter')
|
||||
'link', 'converter')
|
||||
roles[role] = name
|
||||
|
||||
# transfer instrumentation requests from decorated function
|
||||
@@ -1160,7 +1166,7 @@ def _dict_decorators():
|
||||
l.pop('Unspecified')
|
||||
return l
|
||||
|
||||
if util.py3k:
|
||||
if util.py3k_warning:
|
||||
_set_binop_bases = (set, frozenset)
|
||||
else:
|
||||
import sets
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# orm/dependency.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 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
|
||||
@@ -8,8 +8,7 @@
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy import sql, util
|
||||
import sqlalchemy.exceptions as sa_exc
|
||||
from sqlalchemy import sql, util, exc as sa_exc
|
||||
from sqlalchemy.orm import attributes, exc, sync, unitofwork, \
|
||||
util as mapperutil
|
||||
from sqlalchemy.orm.interfaces import ONETOMANY, MANYTOONE, MANYTOMANY
|
||||
@@ -26,9 +25,15 @@ class DependencyProcessor(object):
|
||||
self.passive_deletes = prop.passive_deletes
|
||||
self.passive_updates = prop.passive_updates
|
||||
self.enable_typechecks = prop.enable_typechecks
|
||||
self._passive_delete_flag = self.passive_deletes and \
|
||||
attributes.PASSIVE_NO_INITIALIZE or \
|
||||
attributes.PASSIVE_OFF
|
||||
if self.passive_deletes:
|
||||
self._passive_delete_flag = attributes.PASSIVE_NO_INITIALIZE
|
||||
else:
|
||||
self._passive_delete_flag = attributes.PASSIVE_OFF
|
||||
if self.passive_updates:
|
||||
self._passive_update_flag = attributes.PASSIVE_NO_INITIALIZE
|
||||
else:
|
||||
self._passive_update_flag= attributes.PASSIVE_OFF
|
||||
|
||||
self.key = prop.key
|
||||
if not self.prop.synchronize_pairs:
|
||||
raise sa_exc.ArgumentError(
|
||||
@@ -53,7 +58,7 @@ class DependencyProcessor(object):
|
||||
"""establish actions and dependencies related to a flush.
|
||||
|
||||
These actions will operate on all relevant states in
|
||||
the aggreagte.
|
||||
the aggregate.
|
||||
|
||||
"""
|
||||
uow.register_preprocessor(self, True)
|
||||
@@ -154,10 +159,8 @@ class DependencyProcessor(object):
|
||||
# detect if there's anything changed or loaded
|
||||
# by a preprocessor on this state/attribute. if not,
|
||||
# we should be able to skip it entirely.
|
||||
sum_ = uow.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=True).sum()
|
||||
sum_ = state.manager[self.key].impl.get_all_pending(state, state.dict)
|
||||
|
||||
if not sum_:
|
||||
continue
|
||||
|
||||
@@ -179,9 +182,7 @@ class DependencyProcessor(object):
|
||||
|
||||
if child_in_cycles:
|
||||
child_actions = []
|
||||
for child_state in sum_:
|
||||
if child_state is None:
|
||||
continue
|
||||
for child_state, child in sum_:
|
||||
if child_state not in uow.states:
|
||||
child_action = (None, None)
|
||||
else:
|
||||
@@ -223,7 +224,12 @@ class DependencyProcessor(object):
|
||||
pass
|
||||
|
||||
def prop_has_changes(self, uowcommit, states, isdelete):
|
||||
passive = not isdelete or self.passive_deletes
|
||||
if not isdelete or self.passive_deletes:
|
||||
passive = attributes.PASSIVE_NO_INITIALIZE
|
||||
elif self.direction is MANYTOONE:
|
||||
passive = attributes.PASSIVE_NO_FETCH_RELATED
|
||||
else:
|
||||
passive = attributes.PASSIVE_OFF
|
||||
|
||||
for s in states:
|
||||
# TODO: add a high speed method
|
||||
@@ -232,30 +238,45 @@ class DependencyProcessor(object):
|
||||
history = uowcommit.get_attribute_history(
|
||||
s,
|
||||
self.key,
|
||||
passive=passive)
|
||||
passive)
|
||||
if history and not history.empty():
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return states and \
|
||||
not self.prop._is_self_referential and \
|
||||
self.mapper in uowcommit.mappers
|
||||
|
||||
def _verify_canload(self, state):
|
||||
if state is not None and \
|
||||
not self.mapper._canload(state,
|
||||
allow_subtypes=not self.enable_typechecks):
|
||||
if self.mapper._canload(state, allow_subtypes=True):
|
||||
raise exc.FlushError(
|
||||
"Attempting to flush an item of type %s on collection '%s', "
|
||||
"which is not the expected type %s. Configure mapper '%s' "
|
||||
"to load this subtype polymorphically, or set "
|
||||
"enable_typechecks=False to allow subtypes. "
|
||||
"Mismatched typeloading may cause bi-directional "
|
||||
"relationships (backrefs) to not function properly." %
|
||||
(state.class_, self.prop, self.mapper.class_, self.mapper))
|
||||
raise exc.FlushError('Attempting to flush an item of type '
|
||||
'%(x)s as a member of collection '
|
||||
'"%(y)s". Expected an object of type '
|
||||
'%(z)s or a polymorphic subclass of '
|
||||
'this type. If %(x)s is a subclass of '
|
||||
'%(z)s, configure mapper "%(zm)s" to '
|
||||
'load this subtype polymorphically, or '
|
||||
'set enable_typechecks=False to allow '
|
||||
'any subtype to be accepted for flush. '
|
||||
% {
|
||||
'x': state.class_,
|
||||
'y': self.prop,
|
||||
'z': self.mapper.class_,
|
||||
'zm': self.mapper,
|
||||
})
|
||||
else:
|
||||
raise exc.FlushError(
|
||||
"Attempting to flush an item of type %s on collection '%s', "
|
||||
"whose mapper does not inherit from that of %s." %
|
||||
(state.class_, self.prop, self.mapper.class_))
|
||||
'Attempting to flush an item of type '
|
||||
'%(x)s as a member of collection '
|
||||
'"%(y)s". Expected an object of type '
|
||||
'%(z)s or a polymorphic subclass of '
|
||||
'this type.' % {
|
||||
'x': state.class_,
|
||||
'y': self.prop,
|
||||
'z': self.mapper.class_,
|
||||
})
|
||||
|
||||
def _synchronize(self, state, child, associationrow,
|
||||
clearkeys, uowcommit):
|
||||
@@ -397,7 +418,7 @@ class OneToManyDP(DependencyProcessor):
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=self._passive_delete_flag)
|
||||
self._passive_delete_flag)
|
||||
if history:
|
||||
for child in history.deleted:
|
||||
if child is not None and self.hasparent(child) is False:
|
||||
@@ -409,7 +430,9 @@ class OneToManyDP(DependencyProcessor):
|
||||
if should_null_fks:
|
||||
for child in history.unchanged:
|
||||
if child is not None:
|
||||
uowcommit.register_object(child)
|
||||
uowcommit.register_object(child,
|
||||
operation="delete", prop=self.prop)
|
||||
|
||||
|
||||
|
||||
def presort_saves(self, uowcommit, states):
|
||||
@@ -418,27 +441,36 @@ class OneToManyDP(DependencyProcessor):
|
||||
for state in states:
|
||||
pks_changed = self._pks_changed(uowcommit, state)
|
||||
|
||||
if not pks_changed or self.passive_updates:
|
||||
passive = attributes.PASSIVE_NO_INITIALIZE
|
||||
else:
|
||||
passive = attributes.PASSIVE_OFF
|
||||
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=not pks_changed
|
||||
or self.passive_updates)
|
||||
passive)
|
||||
if history:
|
||||
for child in history.added:
|
||||
if child is not None:
|
||||
uowcommit.register_object(child, cancel_delete=True)
|
||||
uowcommit.register_object(child, cancel_delete=True,
|
||||
operation="add",
|
||||
prop=self.prop)
|
||||
|
||||
children_added.update(history.added)
|
||||
|
||||
for child in history.deleted:
|
||||
if not self.cascade.delete_orphan:
|
||||
uowcommit.register_object(child, isdelete=False)
|
||||
uowcommit.register_object(child, isdelete=False,
|
||||
operation='delete',
|
||||
prop=self.prop)
|
||||
elif self.hasparent(child) is False:
|
||||
uowcommit.register_object(child, isdelete=True)
|
||||
for c, m in self.mapper.cascade_iterator(
|
||||
uowcommit.register_object(child, isdelete=True,
|
||||
operation="delete", prop=self.prop)
|
||||
for c, m, st_, dct_ in self.mapper.cascade_iterator(
|
||||
'delete', child):
|
||||
uowcommit.register_object(
|
||||
attributes.instance_state(c),
|
||||
st_,
|
||||
isdelete=True)
|
||||
|
||||
if pks_changed:
|
||||
@@ -448,7 +480,9 @@ class OneToManyDP(DependencyProcessor):
|
||||
uowcommit.register_object(
|
||||
child,
|
||||
False,
|
||||
self.passive_updates)
|
||||
self.passive_updates,
|
||||
operation="pk change",
|
||||
prop=self.prop)
|
||||
|
||||
def process_deletes(self, uowcommit, states):
|
||||
# head object is being deleted, and we manage its list of
|
||||
@@ -464,7 +498,7 @@ class OneToManyDP(DependencyProcessor):
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=self._passive_delete_flag)
|
||||
self._passive_delete_flag)
|
||||
if history:
|
||||
for child in history.deleted:
|
||||
if child is not None and \
|
||||
@@ -498,9 +532,10 @@ class OneToManyDP(DependencyProcessor):
|
||||
|
||||
def process_saves(self, uowcommit, states):
|
||||
for state in states:
|
||||
history = uowcommit.get_attribute_history(state,
|
||||
self.key,
|
||||
passive=True)
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
attributes.PASSIVE_NO_INITIALIZE)
|
||||
if history:
|
||||
for child in history.added:
|
||||
self._synchronize(state, child, None,
|
||||
@@ -644,7 +679,7 @@ class ManyToOneDP(DependencyProcessor):
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=self._passive_delete_flag)
|
||||
self._passive_delete_flag)
|
||||
if history:
|
||||
if self.cascade.delete_orphan:
|
||||
todelete = history.sum()
|
||||
@@ -653,29 +688,32 @@ class ManyToOneDP(DependencyProcessor):
|
||||
for child in todelete:
|
||||
if child is None:
|
||||
continue
|
||||
uowcommit.register_object(child, isdelete=True)
|
||||
for c, m in self.mapper.cascade_iterator(
|
||||
uowcommit.register_object(child, isdelete=True,
|
||||
operation="delete", prop=self.prop)
|
||||
for c, m, st_, dct_ in self.mapper.cascade_iterator(
|
||||
'delete', child):
|
||||
uowcommit.register_object(
|
||||
attributes.instance_state(c), isdelete=True)
|
||||
st_, isdelete=True)
|
||||
|
||||
def presort_saves(self, uowcommit, states):
|
||||
for state in states:
|
||||
uowcommit.register_object(state)
|
||||
uowcommit.register_object(state, operation="add", prop=self.prop)
|
||||
if self.cascade.delete_orphan:
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=self._passive_delete_flag)
|
||||
self._passive_delete_flag)
|
||||
if history:
|
||||
ret = True
|
||||
for child in history.deleted:
|
||||
if self.hasparent(child) is False:
|
||||
uowcommit.register_object(child, isdelete=True)
|
||||
for c, m in self.mapper.cascade_iterator(
|
||||
uowcommit.register_object(child, isdelete=True,
|
||||
operation="delete", prop=self.prop)
|
||||
|
||||
for c, m, st_, dct_ in self.mapper.cascade_iterator(
|
||||
'delete', child):
|
||||
uowcommit.register_object(
|
||||
attributes.instance_state(c),
|
||||
st_,
|
||||
isdelete=True)
|
||||
|
||||
def process_deletes(self, uowcommit, states):
|
||||
@@ -692,28 +730,39 @@ class ManyToOneDP(DependencyProcessor):
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=self._passive_delete_flag)
|
||||
self._passive_delete_flag)
|
||||
if history:
|
||||
self._post_update(state, uowcommit, history.sum())
|
||||
|
||||
def process_saves(self, uowcommit, states):
|
||||
for state in states:
|
||||
history = uowcommit.get_attribute_history(state,
|
||||
self.key,
|
||||
passive=True)
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
attributes.PASSIVE_NO_INITIALIZE)
|
||||
if history:
|
||||
for child in history.added:
|
||||
self._synchronize(state, child, None, False, uowcommit)
|
||||
self._synchronize(state, child, None, False,
|
||||
uowcommit, "add")
|
||||
|
||||
if self.post_update:
|
||||
self._post_update(state, uowcommit, history.sum())
|
||||
|
||||
def _synchronize(self, state, child, associationrow,
|
||||
clearkeys, uowcommit):
|
||||
def _synchronize(self, state, child, associationrow,
|
||||
clearkeys, uowcommit, operation=None):
|
||||
if state is None or \
|
||||
(not self.post_update and uowcommit.is_deleted(state)):
|
||||
return
|
||||
|
||||
if operation is not None and \
|
||||
child is not None and \
|
||||
not uowcommit.session._contains_state(child):
|
||||
util.warn(
|
||||
"Object of type %s not in session, %s "
|
||||
"operation along '%s' won't proceed" %
|
||||
(mapperutil.state_class_str(child), operation, self.prop))
|
||||
return
|
||||
|
||||
if clearkeys or child is None:
|
||||
sync.clear(state, self.parent, self.prop.synchronize_pairs)
|
||||
else:
|
||||
@@ -811,7 +860,7 @@ class DetectKeySwitch(DependencyProcessor):
|
||||
continue
|
||||
dict_ = state.dict
|
||||
related = state.get_impl(self.key).get(state, dict_,
|
||||
passive=self.passive_updates)
|
||||
passive=self._passive_update_flag)
|
||||
if related is not attributes.PASSIVE_NO_RESULT and \
|
||||
related is not None:
|
||||
related_state = attributes.instance_state(dict_[self.key])
|
||||
@@ -826,7 +875,7 @@ class DetectKeySwitch(DependencyProcessor):
|
||||
uowcommit, self.passive_updates)
|
||||
|
||||
def _pks_changed(self, uowcommit, state):
|
||||
return state.has_identity and sync.source_modified(uowcommit,
|
||||
return bool(state.key) and sync.source_modified(uowcommit,
|
||||
state,
|
||||
self.mapper,
|
||||
self.prop.synchronize_pairs)
|
||||
@@ -891,7 +940,7 @@ class ManyToManyDP(DependencyProcessor):
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=self._passive_delete_flag)
|
||||
self._passive_delete_flag)
|
||||
|
||||
def presort_saves(self, uowcommit, states):
|
||||
if not self.passive_updates:
|
||||
@@ -903,7 +952,7 @@ class ManyToManyDP(DependencyProcessor):
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
False)
|
||||
attributes.PASSIVE_OFF)
|
||||
|
||||
if not self.cascade.delete_orphan:
|
||||
return
|
||||
@@ -914,16 +963,17 @@ class ManyToManyDP(DependencyProcessor):
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=True)
|
||||
attributes.PASSIVE_NO_INITIALIZE)
|
||||
if history:
|
||||
for child in history.deleted:
|
||||
if self.hasparent(child) is False:
|
||||
uowcommit.register_object(child, isdelete=True)
|
||||
for c, m in self.mapper.cascade_iterator(
|
||||
uowcommit.register_object(child, isdelete=True,
|
||||
operation="delete", prop=self.prop)
|
||||
for c, m, st_, dct_ in self.mapper.cascade_iterator(
|
||||
'delete',
|
||||
child):
|
||||
uowcommit.register_object(
|
||||
attributes.instance_state(c), isdelete=True)
|
||||
st_, isdelete=True)
|
||||
|
||||
def process_deletes(self, uowcommit, states):
|
||||
secondary_delete = []
|
||||
@@ -938,20 +988,20 @@ class ManyToManyDP(DependencyProcessor):
|
||||
history = uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
passive=self._passive_delete_flag)
|
||||
self._passive_delete_flag)
|
||||
if history:
|
||||
for child in history.non_added():
|
||||
if child is None or \
|
||||
(processed is not None and
|
||||
(state, child) in processed) or \
|
||||
not uowcommit.session._contains_state(child):
|
||||
(state, child) in processed):
|
||||
continue
|
||||
associationrow = {}
|
||||
self._synchronize(
|
||||
if not self._synchronize(
|
||||
state,
|
||||
child,
|
||||
associationrow,
|
||||
False, uowcommit)
|
||||
False, uowcommit, "delete"):
|
||||
continue
|
||||
secondary_delete.append(associationrow)
|
||||
|
||||
tmp.update((c, state) for c in history.non_added())
|
||||
@@ -973,8 +1023,12 @@ class ManyToManyDP(DependencyProcessor):
|
||||
for state in states:
|
||||
need_cascade_pks = not self.passive_updates and \
|
||||
self._pks_changed(uowcommit, state)
|
||||
if need_cascade_pks:
|
||||
passive = attributes.PASSIVE_OFF
|
||||
else:
|
||||
passive = attributes.PASSIVE_NO_INITIALIZE
|
||||
history = uowcommit.get_attribute_history(state, self.key,
|
||||
passive=not need_cascade_pks)
|
||||
passive)
|
||||
if history:
|
||||
for child in history.added:
|
||||
if child is None or \
|
||||
@@ -982,22 +1036,23 @@ class ManyToManyDP(DependencyProcessor):
|
||||
(state, child) in processed):
|
||||
continue
|
||||
associationrow = {}
|
||||
self._synchronize(state,
|
||||
if not self._synchronize(state,
|
||||
child,
|
||||
associationrow,
|
||||
False, uowcommit)
|
||||
False, uowcommit, "add"):
|
||||
continue
|
||||
secondary_insert.append(associationrow)
|
||||
for child in history.deleted:
|
||||
if child is None or \
|
||||
(processed is not None and
|
||||
(state, child) in processed) or \
|
||||
not uowcommit.session._contains_state(child):
|
||||
(state, child) in processed):
|
||||
continue
|
||||
associationrow = {}
|
||||
self._synchronize(state,
|
||||
if not self._synchronize(state,
|
||||
child,
|
||||
associationrow,
|
||||
False, uowcommit)
|
||||
False, uowcommit, "delete"):
|
||||
continue
|
||||
secondary_delete.append(associationrow)
|
||||
|
||||
tmp.update((c, state)
|
||||
@@ -1070,9 +1125,18 @@ class ManyToManyDP(DependencyProcessor):
|
||||
connection.execute(statement, secondary_insert)
|
||||
|
||||
def _synchronize(self, state, child, associationrow,
|
||||
clearkeys, uowcommit):
|
||||
clearkeys, uowcommit, operation):
|
||||
if associationrow is None:
|
||||
return
|
||||
|
||||
if child is not None and not uowcommit.session._contains_state(child):
|
||||
if not child.deleted:
|
||||
util.warn(
|
||||
"Object of type %s not in session, %s "
|
||||
"operation along '%s' won't proceed" %
|
||||
(mapperutil.state_class_str(child), operation, self.prop))
|
||||
return False
|
||||
|
||||
self._verify_canload(child)
|
||||
|
||||
sync.populate_dict(state, self.parent, associationrow,
|
||||
@@ -1080,6 +1144,8 @@ class ManyToManyDP(DependencyProcessor):
|
||||
sync.populate_dict(child, self.mapper, associationrow,
|
||||
self.prop.secondary_synchronize_pairs)
|
||||
|
||||
return True
|
||||
|
||||
def _pks_changed(self, uowcommit, state):
|
||||
return sync.source_modified(
|
||||
uowcommit,
|
||||
|
||||
@@ -0,0 +1,589 @@
|
||||
# orm/deprecated_interfaces.py
|
||||
# Copyright (C) 2005-2012 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
|
||||
|
||||
from sqlalchemy import event, util
|
||||
from interfaces import EXT_CONTINUE
|
||||
|
||||
|
||||
class MapperExtension(object):
|
||||
"""Base implementation for :class:`.Mapper` event hooks.
|
||||
|
||||
.. note::
|
||||
|
||||
:class:`.MapperExtension` is deprecated. Please
|
||||
refer to :func:`.event.listen` as well as
|
||||
:class:`.MapperEvents`.
|
||||
|
||||
New extension classes subclass :class:`.MapperExtension` and are specified
|
||||
using the ``extension`` mapper() argument, which is a single
|
||||
:class:`.MapperExtension` or a list of such::
|
||||
|
||||
from sqlalchemy.orm.interfaces import MapperExtension
|
||||
|
||||
class MyExtension(MapperExtension):
|
||||
def before_insert(self, mapper, connection, instance):
|
||||
print "instance %s before insert !" % instance
|
||||
|
||||
m = mapper(User, users_table, extension=MyExtension())
|
||||
|
||||
A single mapper can maintain a chain of ``MapperExtension``
|
||||
objects. When a particular mapping event occurs, the
|
||||
corresponding method on each ``MapperExtension`` is invoked
|
||||
serially, and each method has the ability to halt the chain
|
||||
from proceeding further::
|
||||
|
||||
m = mapper(User, users_table, extension=[ext1, ext2, ext3])
|
||||
|
||||
Each ``MapperExtension`` method returns the symbol
|
||||
EXT_CONTINUE by default. This symbol generally means "move
|
||||
to the next ``MapperExtension`` for processing". For methods
|
||||
that return objects like translated rows or new object
|
||||
instances, EXT_CONTINUE means the result of the method
|
||||
should be ignored. In some cases it's required for a
|
||||
default mapper activity to be performed, such as adding a
|
||||
new instance to a result list.
|
||||
|
||||
The symbol EXT_STOP has significance within a chain
|
||||
of ``MapperExtension`` objects that the chain will be stopped
|
||||
when this symbol is returned. Like EXT_CONTINUE, it also
|
||||
has additional significance in some cases that a default
|
||||
mapper activity will not be performed.
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _adapt_instrument_class(cls, self, listener):
|
||||
cls._adapt_listener_methods(self, listener, ('instrument_class',))
|
||||
|
||||
@classmethod
|
||||
def _adapt_listener(cls, self, listener):
|
||||
cls._adapt_listener_methods(
|
||||
self, listener,
|
||||
(
|
||||
'init_instance',
|
||||
'init_failed',
|
||||
'translate_row',
|
||||
'create_instance',
|
||||
'append_result',
|
||||
'populate_instance',
|
||||
'reconstruct_instance',
|
||||
'before_insert',
|
||||
'after_insert',
|
||||
'before_update',
|
||||
'after_update',
|
||||
'before_delete',
|
||||
'after_delete'
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def _adapt_listener_methods(cls, self, listener, methods):
|
||||
|
||||
for meth in methods:
|
||||
me_meth = getattr(MapperExtension, meth)
|
||||
ls_meth = getattr(listener, meth)
|
||||
|
||||
if not util.methods_equivalent(me_meth, ls_meth):
|
||||
if meth == 'reconstruct_instance':
|
||||
def go(ls_meth):
|
||||
def reconstruct(instance, ctx):
|
||||
ls_meth(self, instance)
|
||||
return reconstruct
|
||||
event.listen(self.class_manager, 'load',
|
||||
go(ls_meth), raw=False, propagate=True)
|
||||
elif meth == 'init_instance':
|
||||
def go(ls_meth):
|
||||
def init_instance(instance, args, kwargs):
|
||||
ls_meth(self, self.class_,
|
||||
self.class_manager.original_init,
|
||||
instance, args, kwargs)
|
||||
return init_instance
|
||||
event.listen(self.class_manager, 'init',
|
||||
go(ls_meth), raw=False, propagate=True)
|
||||
elif meth == 'init_failed':
|
||||
def go(ls_meth):
|
||||
def init_failed(instance, args, kwargs):
|
||||
util.warn_exception(ls_meth, self, self.class_,
|
||||
self.class_manager.original_init,
|
||||
instance, args, kwargs)
|
||||
|
||||
return init_failed
|
||||
event.listen(self.class_manager, 'init_failure',
|
||||
go(ls_meth), raw=False, propagate=True)
|
||||
else:
|
||||
event.listen(self, "%s" % meth, ls_meth,
|
||||
raw=False, retval=True, propagate=True)
|
||||
|
||||
|
||||
def instrument_class(self, mapper, class_):
|
||||
"""Receive a class when the mapper is first constructed, and has
|
||||
applied instrumentation to the mapped class.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def init_instance(self, mapper, class_, oldinit, instance, args, kwargs):
|
||||
"""Receive an instance when it's constructor is called.
|
||||
|
||||
This method is only called during a userland construction of
|
||||
an object. It is not called when an object is loaded from the
|
||||
database.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def init_failed(self, mapper, class_, oldinit, instance, args, kwargs):
|
||||
"""Receive an instance when it's constructor has been called,
|
||||
and raised an exception.
|
||||
|
||||
This method is only called during a userland construction of
|
||||
an object. It is not called when an object is loaded from the
|
||||
database.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def translate_row(self, mapper, context, row):
|
||||
"""Perform pre-processing on the given result row and return a
|
||||
new row instance.
|
||||
|
||||
This is called when the mapper first receives a row, before
|
||||
the object identity or the instance itself has been derived
|
||||
from that row. The given row may or may not be a
|
||||
``RowProxy`` object - it will always be a dictionary-like
|
||||
object which contains mapped columns as keys. The
|
||||
returned object should also be a dictionary-like object
|
||||
which recognizes mapped columns as keys.
|
||||
|
||||
If the ultimate return value is EXT_CONTINUE, the row
|
||||
is not translated.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def create_instance(self, mapper, selectcontext, row, class_):
|
||||
"""Receive a row when a new object instance is about to be
|
||||
created from that row.
|
||||
|
||||
The method can choose to create the instance itself, or it can return
|
||||
EXT_CONTINUE to indicate normal object creation should take place.
|
||||
|
||||
mapper
|
||||
The mapper doing the operation
|
||||
|
||||
selectcontext
|
||||
The QueryContext generated from the Query.
|
||||
|
||||
row
|
||||
The result row from the database
|
||||
|
||||
class\_
|
||||
The class we are mapping.
|
||||
|
||||
return value
|
||||
A new object instance, or EXT_CONTINUE
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def append_result(self, mapper, selectcontext, row, instance,
|
||||
result, **flags):
|
||||
"""Receive an object instance before that instance is appended
|
||||
to a result list.
|
||||
|
||||
If this method returns EXT_CONTINUE, result appending will proceed
|
||||
normally. if this method returns any other value or None,
|
||||
result appending will not proceed for this instance, giving
|
||||
this extension an opportunity to do the appending itself, if
|
||||
desired.
|
||||
|
||||
mapper
|
||||
The mapper doing the operation.
|
||||
|
||||
selectcontext
|
||||
The QueryContext generated from the Query.
|
||||
|
||||
row
|
||||
The result row from the database.
|
||||
|
||||
instance
|
||||
The object instance to be appended to the result.
|
||||
|
||||
result
|
||||
List to which results are being appended.
|
||||
|
||||
\**flags
|
||||
extra information about the row, same as criterion in
|
||||
``create_row_processor()`` method of
|
||||
:class:`~sqlalchemy.orm.interfaces.MapperProperty`
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def populate_instance(self, mapper, selectcontext, row,
|
||||
instance, **flags):
|
||||
"""Receive an instance before that instance has
|
||||
its attributes populated.
|
||||
|
||||
This usually corresponds to a newly loaded instance but may
|
||||
also correspond to an already-loaded instance which has
|
||||
unloaded attributes to be populated. The method may be called
|
||||
many times for a single instance, as multiple result rows are
|
||||
used to populate eagerly loaded collections.
|
||||
|
||||
If this method returns EXT_CONTINUE, instance population will
|
||||
proceed normally. If any other value or None is returned,
|
||||
instance population will not proceed, giving this extension an
|
||||
opportunity to populate the instance itself, if desired.
|
||||
|
||||
As of 0.5, most usages of this hook are obsolete. For a
|
||||
generic "object has been newly created from a row" hook, use
|
||||
``reconstruct_instance()``, or the ``@orm.reconstructor``
|
||||
decorator.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def reconstruct_instance(self, mapper, instance):
|
||||
"""Receive an object instance after it has been created via
|
||||
``__new__``, and after initial attribute population has
|
||||
occurred.
|
||||
|
||||
This typically occurs when the instance is created based on
|
||||
incoming result rows, and is only called once for that
|
||||
instance's lifetime.
|
||||
|
||||
Note that during a result-row load, this method is called upon
|
||||
the first row received for this instance. Note that some
|
||||
attributes and collections may or may not be loaded or even
|
||||
initialized, depending on what's present in the result rows.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def before_insert(self, mapper, connection, instance):
|
||||
"""Receive an object instance before that instance is inserted
|
||||
into its table.
|
||||
|
||||
This is a good place to set up primary key values and such
|
||||
that aren't handled otherwise.
|
||||
|
||||
Column-based attributes can be modified within this method
|
||||
which will result in the new value being inserted. However
|
||||
*no* changes to the overall flush plan can be made, and
|
||||
manipulation of the ``Session`` will not have the desired effect.
|
||||
To manipulate the ``Session`` within an extension, use
|
||||
``SessionExtension``.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def after_insert(self, mapper, connection, instance):
|
||||
"""Receive an object instance after that instance is inserted.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def before_update(self, mapper, connection, instance):
|
||||
"""Receive an object instance before that instance is updated.
|
||||
|
||||
Note that this method is called for all instances that are marked as
|
||||
"dirty", even those which have no net changes to their column-based
|
||||
attributes. An object is marked as dirty when any of its column-based
|
||||
attributes have a "set attribute" operation called or when any of its
|
||||
collections are modified. If, at update time, no column-based
|
||||
attributes have any net changes, no UPDATE statement will be issued.
|
||||
This means that an instance being sent to before_update is *not* a
|
||||
guarantee that an UPDATE statement will be issued (although you can
|
||||
affect the outcome here).
|
||||
|
||||
To detect if the column-based attributes on the object have net
|
||||
changes, and will therefore generate an UPDATE statement, use
|
||||
``object_session(instance).is_modified(instance,
|
||||
include_collections=False)``.
|
||||
|
||||
Column-based attributes can be modified within this method
|
||||
which will result in the new value being updated. However
|
||||
*no* changes to the overall flush plan can be made, and
|
||||
manipulation of the ``Session`` will not have the desired effect.
|
||||
To manipulate the ``Session`` within an extension, use
|
||||
``SessionExtension``.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def after_update(self, mapper, connection, instance):
|
||||
"""Receive an object instance after that instance is updated.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def before_delete(self, mapper, connection, instance):
|
||||
"""Receive an object instance before that instance is deleted.
|
||||
|
||||
Note that *no* changes to the overall flush plan can be made
|
||||
here; and manipulation of the ``Session`` will not have the
|
||||
desired effect. To manipulate the ``Session`` within an
|
||||
extension, use ``SessionExtension``.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def after_delete(self, mapper, connection, instance):
|
||||
"""Receive an object instance after that instance is deleted.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
class SessionExtension(object):
|
||||
|
||||
"""Base implementation for :class:`.Session` event hooks.
|
||||
|
||||
.. note::
|
||||
|
||||
:class:`.SessionExtension` is deprecated. Please
|
||||
refer to :func:`.event.listen` as well as
|
||||
:class:`.SessionEvents`.
|
||||
|
||||
Subclasses may be installed into a :class:`.Session` (or
|
||||
:func:`.sessionmaker`) using the ``extension`` keyword
|
||||
argument::
|
||||
|
||||
from sqlalchemy.orm.interfaces import SessionExtension
|
||||
|
||||
class MySessionExtension(SessionExtension):
|
||||
def before_commit(self, session):
|
||||
print "before commit!"
|
||||
|
||||
Session = sessionmaker(extension=MySessionExtension())
|
||||
|
||||
The same :class:`.SessionExtension` instance can be used
|
||||
with any number of sessions.
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _adapt_listener(cls, self, listener):
|
||||
for meth in [
|
||||
'before_commit',
|
||||
'after_commit',
|
||||
'after_rollback',
|
||||
'before_flush',
|
||||
'after_flush',
|
||||
'after_flush_postexec',
|
||||
'after_begin',
|
||||
'after_attach',
|
||||
'after_bulk_update',
|
||||
'after_bulk_delete',
|
||||
]:
|
||||
me_meth = getattr(SessionExtension, meth)
|
||||
ls_meth = getattr(listener, meth)
|
||||
|
||||
if not util.methods_equivalent(me_meth, ls_meth):
|
||||
event.listen(self, meth, getattr(listener, meth))
|
||||
|
||||
def before_commit(self, session):
|
||||
"""Execute right before commit is called.
|
||||
|
||||
Note that this may not be per-flush if a longer running
|
||||
transaction is ongoing."""
|
||||
|
||||
def after_commit(self, session):
|
||||
"""Execute after a commit has occurred.
|
||||
|
||||
Note that this may not be per-flush if a longer running
|
||||
transaction is ongoing."""
|
||||
|
||||
def after_rollback(self, session):
|
||||
"""Execute after a rollback has occurred.
|
||||
|
||||
Note that this may not be per-flush if a longer running
|
||||
transaction is ongoing."""
|
||||
|
||||
def before_flush( self, session, flush_context, instances):
|
||||
"""Execute before flush process has started.
|
||||
|
||||
`instances` is an optional list of objects which were passed to
|
||||
the ``flush()`` method. """
|
||||
|
||||
def after_flush(self, session, flush_context):
|
||||
"""Execute after flush has completed, but before commit has been
|
||||
called.
|
||||
|
||||
Note that the session's state is still in pre-flush, i.e. 'new',
|
||||
'dirty', and 'deleted' lists still show pre-flush state as well
|
||||
as the history settings on instance attributes."""
|
||||
|
||||
def after_flush_postexec(self, session, flush_context):
|
||||
"""Execute after flush has completed, and after the post-exec
|
||||
state occurs.
|
||||
|
||||
This will be when the 'new', 'dirty', and 'deleted' lists are in
|
||||
their final state. An actual commit() may or may not have
|
||||
occurred, depending on whether or not the flush started its own
|
||||
transaction or participated in a larger transaction. """
|
||||
|
||||
def after_begin( self, session, transaction, connection):
|
||||
"""Execute after a transaction is begun on a connection
|
||||
|
||||
`transaction` is the SessionTransaction. This method is called
|
||||
after an engine level transaction is begun on a connection. """
|
||||
|
||||
def after_attach(self, session, instance):
|
||||
"""Execute after an instance is attached to a session.
|
||||
|
||||
This is called after an add, delete or merge. """
|
||||
|
||||
def after_bulk_update( self, session, query, query_context, result):
|
||||
"""Execute after a bulk update operation to the session.
|
||||
|
||||
This is called after a session.query(...).update()
|
||||
|
||||
`query` is the query object that this update operation was
|
||||
called on. `query_context` was the query context object.
|
||||
`result` is the result object returned from the bulk operation.
|
||||
"""
|
||||
|
||||
def after_bulk_delete( self, session, query, query_context, result):
|
||||
"""Execute after a bulk delete operation to the session.
|
||||
|
||||
This is called after a session.query(...).delete()
|
||||
|
||||
`query` is the query object that this delete operation was
|
||||
called on. `query_context` was the query context object.
|
||||
`result` is the result object returned from the bulk operation.
|
||||
"""
|
||||
|
||||
|
||||
class AttributeExtension(object):
|
||||
"""Base implementation for :class:`.AttributeImpl` event hooks, events
|
||||
that fire upon attribute mutations in user code.
|
||||
|
||||
.. note::
|
||||
|
||||
:class:`.AttributeExtension` is deprecated. Please
|
||||
refer to :func:`.event.listen` as well as
|
||||
:class:`.AttributeEvents`.
|
||||
|
||||
:class:`.AttributeExtension` is used to listen for set,
|
||||
remove, and append events on individual mapped attributes.
|
||||
It is established on an individual mapped attribute using
|
||||
the `extension` argument, available on
|
||||
:func:`.column_property`, :func:`.relationship`, and
|
||||
others::
|
||||
|
||||
from sqlalchemy.orm.interfaces import AttributeExtension
|
||||
from sqlalchemy.orm import mapper, relationship, column_property
|
||||
|
||||
class MyAttrExt(AttributeExtension):
|
||||
def append(self, state, value, initiator):
|
||||
print "append event !"
|
||||
return value
|
||||
|
||||
def set(self, state, value, oldvalue, initiator):
|
||||
print "set event !"
|
||||
return value
|
||||
|
||||
mapper(SomeClass, sometable, properties={
|
||||
'foo':column_property(sometable.c.foo, extension=MyAttrExt()),
|
||||
'bar':relationship(Bar, extension=MyAttrExt())
|
||||
})
|
||||
|
||||
Note that the :class:`.AttributeExtension` methods
|
||||
:meth:`~.AttributeExtension.append` and
|
||||
:meth:`~.AttributeExtension.set` need to return the
|
||||
``value`` parameter. The returned value is used as the
|
||||
effective value, and allows the extension to change what is
|
||||
ultimately persisted.
|
||||
|
||||
AttributeExtension is assembled within the descriptors associated
|
||||
with a mapped class.
|
||||
|
||||
"""
|
||||
|
||||
active_history = True
|
||||
"""indicates that the set() method would like to receive the 'old' value,
|
||||
even if it means firing lazy callables.
|
||||
|
||||
Note that ``active_history`` can also be set directly via
|
||||
:func:`.column_property` and :func:`.relationship`.
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _adapt_listener(cls, self, listener):
|
||||
event.listen(self, 'append', listener.append,
|
||||
active_history=listener.active_history,
|
||||
raw=True, retval=True)
|
||||
event.listen(self, 'remove', listener.remove,
|
||||
active_history=listener.active_history,
|
||||
raw=True, retval=True)
|
||||
event.listen(self, 'set', listener.set,
|
||||
active_history=listener.active_history,
|
||||
raw=True, retval=True)
|
||||
|
||||
def append(self, state, value, initiator):
|
||||
"""Receive a collection append event.
|
||||
|
||||
The returned value will be used as the actual value to be
|
||||
appended.
|
||||
|
||||
"""
|
||||
return value
|
||||
|
||||
def remove(self, state, value, initiator):
|
||||
"""Receive a remove event.
|
||||
|
||||
No return value is defined.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def set(self, state, value, oldvalue, initiator):
|
||||
"""Receive a set event.
|
||||
|
||||
The returned value will be used as the actual value to be
|
||||
set.
|
||||
|
||||
"""
|
||||
return value
|
||||
|
||||
|
||||
@@ -0,0 +1,422 @@
|
||||
# orm/descriptor_props.py
|
||||
# Copyright (C) 2005-2012 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
|
||||
|
||||
"""Descriptor properties are more "auxiliary" properties
|
||||
that exist as configurational elements, but don't participate
|
||||
as actively in the load/persist ORM loop.
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm.interfaces import \
|
||||
MapperProperty, PropComparator, StrategizedProperty
|
||||
from sqlalchemy.orm.mapper import _none_set
|
||||
from sqlalchemy.orm import attributes, strategies
|
||||
from sqlalchemy import util, sql, exc as sa_exc, event, schema
|
||||
from sqlalchemy.sql import expression
|
||||
properties = util.importlater('sqlalchemy.orm', 'properties')
|
||||
|
||||
class DescriptorProperty(MapperProperty):
|
||||
""":class:`.MapperProperty` which proxies access to a
|
||||
user-defined descriptor."""
|
||||
|
||||
doc = None
|
||||
|
||||
def instrument_class(self, mapper):
|
||||
prop = self
|
||||
|
||||
class _ProxyImpl(object):
|
||||
accepts_scalar_loader = False
|
||||
expire_missing = True
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
if hasattr(prop, 'get_history'):
|
||||
def get_history(self, state, dict_,
|
||||
passive=attributes.PASSIVE_OFF):
|
||||
return prop.get_history(state, dict_, passive)
|
||||
|
||||
if self.descriptor is None:
|
||||
desc = getattr(mapper.class_, self.key, None)
|
||||
if mapper._is_userland_descriptor(desc):
|
||||
self.descriptor = desc
|
||||
|
||||
if self.descriptor is None:
|
||||
def fset(obj, value):
|
||||
setattr(obj, self.name, value)
|
||||
def fdel(obj):
|
||||
delattr(obj, self.name)
|
||||
def fget(obj):
|
||||
return getattr(obj, self.name)
|
||||
|
||||
self.descriptor = property(
|
||||
fget=fget,
|
||||
fset=fset,
|
||||
fdel=fdel,
|
||||
)
|
||||
|
||||
proxy_attr = attributes.\
|
||||
create_proxied_attribute(self.descriptor)\
|
||||
(
|
||||
self.parent.class_,
|
||||
self.key,
|
||||
self.descriptor,
|
||||
lambda: self._comparator_factory(mapper),
|
||||
doc=self.doc
|
||||
)
|
||||
proxy_attr.impl = _ProxyImpl(self.key)
|
||||
mapper.class_manager.instrument_attribute(self.key, proxy_attr)
|
||||
|
||||
|
||||
class CompositeProperty(DescriptorProperty):
|
||||
|
||||
def __init__(self, class_, *attrs, **kwargs):
|
||||
self.attrs = attrs
|
||||
self.composite_class = class_
|
||||
self.active_history = kwargs.get('active_history', False)
|
||||
self.deferred = kwargs.get('deferred', False)
|
||||
self.group = kwargs.get('group', None)
|
||||
self.comparator_factory = kwargs.pop('comparator_factory',
|
||||
self.__class__.Comparator)
|
||||
util.set_creation_order(self)
|
||||
self._create_descriptor()
|
||||
|
||||
def instrument_class(self, mapper):
|
||||
super(CompositeProperty, self).instrument_class(mapper)
|
||||
self._setup_event_handlers()
|
||||
|
||||
def do_init(self):
|
||||
"""Initialization which occurs after the :class:`.CompositeProperty`
|
||||
has been associated with its parent mapper.
|
||||
|
||||
"""
|
||||
self._init_props()
|
||||
self._setup_arguments_on_columns()
|
||||
|
||||
def _create_descriptor(self):
|
||||
"""Create the Python descriptor that will serve as
|
||||
the access point on instances of the mapped class.
|
||||
|
||||
"""
|
||||
|
||||
def fget(instance):
|
||||
dict_ = attributes.instance_dict(instance)
|
||||
state = attributes.instance_state(instance)
|
||||
|
||||
if self.key not in dict_:
|
||||
# key not present. Iterate through related
|
||||
# attributes, retrieve their values. This
|
||||
# ensures they all load.
|
||||
values = [getattr(instance, key) for key in self._attribute_keys]
|
||||
|
||||
# current expected behavior here is that the composite is
|
||||
# created on access if the object is persistent or if
|
||||
# col attributes have non-None. This would be better
|
||||
# if the composite were created unconditionally,
|
||||
# but that would be a behavioral change.
|
||||
if self.key not in dict_ and (
|
||||
state.key is not None or
|
||||
not _none_set.issuperset(values)
|
||||
):
|
||||
dict_[self.key] = self.composite_class(*values)
|
||||
state.manager.dispatch.refresh(state, None, [self.key])
|
||||
|
||||
return dict_.get(self.key, None)
|
||||
|
||||
def fset(instance, value):
|
||||
dict_ = attributes.instance_dict(instance)
|
||||
state = attributes.instance_state(instance)
|
||||
attr = state.manager[self.key]
|
||||
previous = dict_.get(self.key, attributes.NO_VALUE)
|
||||
for fn in attr.dispatch.set:
|
||||
value = fn(state, value, previous, attr.impl)
|
||||
dict_[self.key] = value
|
||||
if value is None:
|
||||
for key in self._attribute_keys:
|
||||
setattr(instance, key, None)
|
||||
else:
|
||||
for key, value in zip(
|
||||
self._attribute_keys,
|
||||
value.__composite_values__()):
|
||||
setattr(instance, key, value)
|
||||
|
||||
def fdel(instance):
|
||||
state = attributes.instance_state(instance)
|
||||
dict_ = attributes.instance_dict(instance)
|
||||
previous = dict_.pop(self.key, attributes.NO_VALUE)
|
||||
attr = state.manager[self.key]
|
||||
attr.dispatch.remove(state, previous, attr.impl)
|
||||
for key in self._attribute_keys:
|
||||
setattr(instance, key, None)
|
||||
|
||||
self.descriptor = property(fget, fset, fdel)
|
||||
|
||||
@util.memoized_property
|
||||
def _comparable_elements(self):
|
||||
return [
|
||||
getattr(self.parent.class_, prop.key)
|
||||
for prop in self.props
|
||||
]
|
||||
|
||||
def _init_props(self):
|
||||
self.props = props = []
|
||||
for attr in self.attrs:
|
||||
if isinstance(attr, basestring):
|
||||
prop = self.parent.get_property(attr)
|
||||
elif isinstance(attr, schema.Column):
|
||||
prop = self.parent._columntoproperty[attr]
|
||||
elif isinstance(attr, attributes.InstrumentedAttribute):
|
||||
prop = attr.property
|
||||
props.append(prop)
|
||||
|
||||
@property
|
||||
def columns(self):
|
||||
return [a for a in self.attrs if isinstance(a, schema.Column)]
|
||||
|
||||
def _setup_arguments_on_columns(self):
|
||||
"""Propagate configuration arguments made on this composite
|
||||
to the target columns, for those that apply.
|
||||
|
||||
"""
|
||||
for prop in self.props:
|
||||
prop.active_history = self.active_history
|
||||
if self.deferred:
|
||||
prop.deferred = self.deferred
|
||||
prop.strategy_class = strategies.DeferredColumnLoader
|
||||
prop.group = self.group
|
||||
|
||||
def _setup_event_handlers(self):
|
||||
"""Establish events that populate/expire the composite attribute."""
|
||||
|
||||
def load_handler(state, *args):
|
||||
dict_ = state.dict
|
||||
|
||||
if self.key in dict_:
|
||||
return
|
||||
|
||||
# if column elements aren't loaded, skip.
|
||||
# __get__() will initiate a load for those
|
||||
# columns
|
||||
for k in self._attribute_keys:
|
||||
if k not in dict_:
|
||||
return
|
||||
|
||||
#assert self.key not in dict_
|
||||
dict_[self.key] = self.composite_class(
|
||||
*[state.dict[key] for key in
|
||||
self._attribute_keys]
|
||||
)
|
||||
|
||||
def expire_handler(state, keys):
|
||||
if keys is None or set(self._attribute_keys).intersection(keys):
|
||||
state.dict.pop(self.key, None)
|
||||
|
||||
def insert_update_handler(mapper, connection, state):
|
||||
"""After an insert or update, some columns may be expired due
|
||||
to server side defaults, or re-populated due to client side
|
||||
defaults. Pop out the composite value here so that it
|
||||
recreates.
|
||||
|
||||
"""
|
||||
|
||||
state.dict.pop(self.key, None)
|
||||
|
||||
event.listen(self.parent, 'after_insert',
|
||||
insert_update_handler, raw=True)
|
||||
event.listen(self.parent, 'after_update',
|
||||
insert_update_handler, raw=True)
|
||||
event.listen(self.parent, 'load', load_handler, raw=True, propagate=True)
|
||||
event.listen(self.parent, 'refresh', load_handler, raw=True, propagate=True)
|
||||
event.listen(self.parent, "expire", expire_handler, raw=True, propagate=True)
|
||||
|
||||
# TODO: need a deserialize hook here
|
||||
|
||||
@util.memoized_property
|
||||
def _attribute_keys(self):
|
||||
return [
|
||||
prop.key for prop in self.props
|
||||
]
|
||||
|
||||
def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF):
|
||||
"""Provided for userland code that uses attributes.get_history()."""
|
||||
|
||||
added = []
|
||||
deleted = []
|
||||
|
||||
has_history = False
|
||||
for prop in self.props:
|
||||
key = prop.key
|
||||
hist = state.manager[key].impl.get_history(state, dict_)
|
||||
if hist.has_changes():
|
||||
has_history = True
|
||||
|
||||
non_deleted = hist.non_deleted()
|
||||
if non_deleted:
|
||||
added.extend(non_deleted)
|
||||
else:
|
||||
added.append(None)
|
||||
if hist.deleted:
|
||||
deleted.extend(hist.deleted)
|
||||
else:
|
||||
deleted.append(None)
|
||||
|
||||
if has_history:
|
||||
return attributes.History(
|
||||
[self.composite_class(*added)],
|
||||
(),
|
||||
[self.composite_class(*deleted)]
|
||||
)
|
||||
else:
|
||||
return attributes.History(
|
||||
(),[self.composite_class(*added)], ()
|
||||
)
|
||||
|
||||
def _comparator_factory(self, mapper):
|
||||
return self.comparator_factory(self)
|
||||
|
||||
class Comparator(PropComparator):
|
||||
def __init__(self, prop, adapter=None):
|
||||
self.prop = self.property = prop
|
||||
self.adapter = adapter
|
||||
|
||||
def __clause_element__(self):
|
||||
if self.adapter:
|
||||
# TODO: test coverage for adapted composite comparison
|
||||
return expression.ClauseList(
|
||||
*[self.adapter(x) for x in self.prop._comparable_elements])
|
||||
else:
|
||||
return expression.ClauseList(*self.prop._comparable_elements)
|
||||
|
||||
__hash__ = None
|
||||
|
||||
def __eq__(self, other):
|
||||
if other is None:
|
||||
values = [None] * len(self.prop._comparable_elements)
|
||||
else:
|
||||
values = other.__composite_values__()
|
||||
return sql.and_(
|
||||
*[a==b for a, b in zip(self.prop._comparable_elements, values)])
|
||||
|
||||
def __ne__(self, other):
|
||||
return sql.not_(self.__eq__(other))
|
||||
|
||||
def __str__(self):
|
||||
return str(self.parent.class_.__name__) + "." + self.key
|
||||
|
||||
class ConcreteInheritedProperty(DescriptorProperty):
|
||||
"""A 'do nothing' :class:`.MapperProperty` that disables
|
||||
an attribute on a concrete subclass that is only present
|
||||
on the inherited mapper, not the concrete classes' mapper.
|
||||
|
||||
Cases where this occurs include:
|
||||
|
||||
* When the superclass mapper is mapped against a
|
||||
"polymorphic union", which includes all attributes from
|
||||
all subclasses.
|
||||
* When a relationship() is configured on an inherited mapper,
|
||||
but not on the subclass mapper. Concrete mappers require
|
||||
that relationship() is configured explicitly on each
|
||||
subclass.
|
||||
|
||||
"""
|
||||
|
||||
def _comparator_factory(self, mapper):
|
||||
comparator_callable = None
|
||||
|
||||
for m in self.parent.iterate_to_root():
|
||||
p = m._props[self.key]
|
||||
if not isinstance(p, ConcreteInheritedProperty):
|
||||
comparator_callable = p.comparator_factory
|
||||
break
|
||||
return comparator_callable
|
||||
|
||||
def __init__(self):
|
||||
def warn():
|
||||
raise AttributeError("Concrete %s does not implement "
|
||||
"attribute %r at the instance level. Add this "
|
||||
"property explicitly to %s." %
|
||||
(self.parent, self.key, self.parent))
|
||||
|
||||
class NoninheritedConcreteProp(object):
|
||||
def __set__(s, obj, value):
|
||||
warn()
|
||||
def __delete__(s, obj):
|
||||
warn()
|
||||
def __get__(s, obj, owner):
|
||||
if obj is None:
|
||||
return self.descriptor
|
||||
warn()
|
||||
self.descriptor = NoninheritedConcreteProp()
|
||||
|
||||
|
||||
class SynonymProperty(DescriptorProperty):
|
||||
|
||||
def __init__(self, name, map_column=None,
|
||||
descriptor=None, comparator_factory=None,
|
||||
doc=None):
|
||||
self.name = name
|
||||
self.map_column = map_column
|
||||
self.descriptor = descriptor
|
||||
self.comparator_factory = comparator_factory
|
||||
self.doc = doc or (descriptor and descriptor.__doc__) or None
|
||||
|
||||
util.set_creation_order(self)
|
||||
|
||||
# TODO: when initialized, check _proxied_property,
|
||||
# emit a warning if its not a column-based property
|
||||
|
||||
@util.memoized_property
|
||||
def _proxied_property(self):
|
||||
return getattr(self.parent.class_, self.name).property
|
||||
|
||||
def _comparator_factory(self, mapper):
|
||||
prop = self._proxied_property
|
||||
|
||||
if self.comparator_factory:
|
||||
comp = self.comparator_factory(prop, mapper)
|
||||
else:
|
||||
comp = prop.comparator_factory(prop, mapper)
|
||||
return comp
|
||||
|
||||
def set_parent(self, parent, init):
|
||||
if self.map_column:
|
||||
# implement the 'map_column' option.
|
||||
if self.key not in parent.mapped_table.c:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Can't compile synonym '%s': no column on table "
|
||||
"'%s' named '%s'"
|
||||
% (self.name, parent.mapped_table.description, self.key))
|
||||
elif parent.mapped_table.c[self.key] in \
|
||||
parent._columntoproperty and \
|
||||
parent._columntoproperty[
|
||||
parent.mapped_table.c[self.key]
|
||||
].key == self.name:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Can't call map_column=True for synonym %r=%r, "
|
||||
"a ColumnProperty already exists keyed to the name "
|
||||
"%r for column %r" %
|
||||
(self.key, self.name, self.name, self.key)
|
||||
)
|
||||
p = properties.ColumnProperty(parent.mapped_table.c[self.key])
|
||||
parent._configure_property(
|
||||
self.name, p,
|
||||
init=init,
|
||||
setparent=True)
|
||||
p._mapped_by_synonym = self.key
|
||||
|
||||
self.parent = parent
|
||||
|
||||
class ComparableProperty(DescriptorProperty):
|
||||
"""Instruments a Python property for use in query expressions."""
|
||||
|
||||
def __init__(self, comparator_factory, descriptor=None, doc=None):
|
||||
self.descriptor = descriptor
|
||||
self.comparator_factory = comparator_factory
|
||||
self.doc = doc or (descriptor and descriptor.__doc__) or None
|
||||
util.set_creation_order(self)
|
||||
|
||||
def _comparator_factory(self, mapper):
|
||||
return self.comparator_factory(self, mapper)
|
||||
@@ -1,5 +1,5 @@
|
||||
# orm/dynamic.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 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
|
||||
@@ -35,9 +35,6 @@ class DynaLoader(strategies.AbstractRelationshipLoader):
|
||||
query_class=self.parent_property.query_class
|
||||
)
|
||||
|
||||
def create_row_processor(self, selectcontext, path, mapper, row, adapter):
|
||||
return None, None, None
|
||||
|
||||
log.class_logger(DynaLoader)
|
||||
|
||||
class DynamicAttributeImpl(attributes.AttributeImpl):
|
||||
@@ -46,9 +43,10 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
|
||||
supports_population = False
|
||||
|
||||
def __init__(self, class_, key, typecallable,
|
||||
target_mapper, order_by, query_class=None, **kwargs):
|
||||
dispatch,
|
||||
target_mapper, order_by, query_class=None, **kw):
|
||||
super(DynamicAttributeImpl, self).\
|
||||
__init__(class_, key, typecallable, **kwargs)
|
||||
__init__(class_, key, typecallable, dispatch, **kw)
|
||||
self.target_mapper = target_mapper
|
||||
self.order_by = order_by
|
||||
if not query_class:
|
||||
@@ -58,41 +56,41 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
|
||||
else:
|
||||
self.query_class = mixin_user_query(query_class)
|
||||
|
||||
def get(self, state, dict_, passive=False):
|
||||
if passive:
|
||||
def get(self, state, dict_, passive=attributes.PASSIVE_OFF):
|
||||
if passive is not attributes.PASSIVE_OFF:
|
||||
return self._get_collection_history(state,
|
||||
passive=True).added_items
|
||||
attributes.PASSIVE_NO_INITIALIZE).added_items
|
||||
else:
|
||||
return self.query_class(self, state)
|
||||
|
||||
def get_collection(self, state, dict_, user_data=None, passive=True):
|
||||
if passive:
|
||||
def get_collection(self, state, dict_, user_data=None,
|
||||
passive=attributes.PASSIVE_NO_INITIALIZE):
|
||||
if passive is not attributes.PASSIVE_OFF:
|
||||
return self._get_collection_history(state,
|
||||
passive=passive).added_items
|
||||
passive).added_items
|
||||
else:
|
||||
history = self._get_collection_history(state,
|
||||
passive=passive)
|
||||
history = self._get_collection_history(state, passive)
|
||||
return history.added_items + history.unchanged_items
|
||||
|
||||
def fire_append_event(self, state, dict_, value, initiator):
|
||||
collection_history = self._modified_event(state, dict_)
|
||||
collection_history.added_items.append(value)
|
||||
|
||||
for ext in self.extensions:
|
||||
ext.append(state, value, initiator or self)
|
||||
for fn in self.dispatch.append:
|
||||
value = fn(state, value, initiator or self)
|
||||
|
||||
if self.trackparent and value is not None:
|
||||
self.sethasparent(attributes.instance_state(value), True)
|
||||
self.sethasparent(attributes.instance_state(value), state, True)
|
||||
|
||||
def fire_remove_event(self, state, dict_, value, initiator):
|
||||
collection_history = self._modified_event(state, dict_)
|
||||
collection_history.deleted_items.append(value)
|
||||
|
||||
if self.trackparent and value is not None:
|
||||
self.sethasparent(attributes.instance_state(value), False)
|
||||
self.sethasparent(attributes.instance_state(value), state, False)
|
||||
|
||||
for ext in self.extensions:
|
||||
ext.remove(state, value, initiator or self)
|
||||
for fn in self.dispatch.remove:
|
||||
fn(state, value, initiator or self)
|
||||
|
||||
def _modified_event(self, state, dict_):
|
||||
|
||||
@@ -100,23 +98,25 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
|
||||
state.committed_state[self.key] = CollectionHistory(self, state)
|
||||
|
||||
state.modified_event(dict_,
|
||||
self,
|
||||
False,
|
||||
attributes.NEVER_SET,
|
||||
passive=attributes.PASSIVE_NO_INITIALIZE)
|
||||
self,
|
||||
attributes.NEVER_SET)
|
||||
|
||||
# this is a hack to allow the _base.ComparableEntity fixture
|
||||
# this is a hack to allow the fixtures.ComparableEntity fixture
|
||||
# to work
|
||||
dict_[self.key] = True
|
||||
return state.committed_state[self.key]
|
||||
|
||||
def set(self, state, dict_, value, initiator,
|
||||
passive=attributes.PASSIVE_OFF):
|
||||
passive=attributes.PASSIVE_OFF,
|
||||
check_old=None, pop=False):
|
||||
if initiator and initiator.parent_token is self.parent_token:
|
||||
return
|
||||
|
||||
if pop and value is None:
|
||||
return
|
||||
self._set_iterable(state, dict_, value)
|
||||
|
||||
|
||||
def _set_iterable(self, state, dict_, iterable, adapter=None):
|
||||
collection_history = self._modified_event(state, dict_)
|
||||
new_values = list(iterable)
|
||||
@@ -136,27 +136,37 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
|
||||
raise NotImplementedError("Dynamic attributes don't support "
|
||||
"collection population.")
|
||||
|
||||
def get_history(self, state, dict_, passive=False):
|
||||
def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF):
|
||||
c = self._get_collection_history(state, passive)
|
||||
return attributes.History(c.added_items, c.unchanged_items,
|
||||
c.deleted_items)
|
||||
|
||||
def _get_collection_history(self, state, passive=False):
|
||||
def get_all_pending(self, state, dict_):
|
||||
c = self._get_collection_history(state, True)
|
||||
return [
|
||||
(attributes.instance_state(x), x)
|
||||
for x in
|
||||
c.added_items + c.unchanged_items + c.deleted_items
|
||||
]
|
||||
|
||||
def _get_collection_history(self, state, passive=attributes.PASSIVE_OFF):
|
||||
if self.key in state.committed_state:
|
||||
c = state.committed_state[self.key]
|
||||
else:
|
||||
c = CollectionHistory(self, state)
|
||||
|
||||
if not passive:
|
||||
if passive is attributes.PASSIVE_OFF:
|
||||
return CollectionHistory(self, state, apply_to=c)
|
||||
else:
|
||||
return c
|
||||
|
||||
def append(self, state, dict_, value, initiator, passive=False):
|
||||
def append(self, state, dict_, value, initiator,
|
||||
passive=attributes.PASSIVE_OFF):
|
||||
if initiator is not self:
|
||||
self.fire_append_event(state, dict_, value, initiator)
|
||||
|
||||
def remove(self, state, dict_, value, initiator, passive=False):
|
||||
def remove(self, state, dict_, value, initiator,
|
||||
passive=attributes.PASSIVE_OFF):
|
||||
if initiator is not self:
|
||||
self.fire_remove_event(state, dict_, value, initiator)
|
||||
|
||||
@@ -192,7 +202,7 @@ class AppenderMixin(object):
|
||||
self.attr = attr
|
||||
|
||||
mapper = object_mapper(instance)
|
||||
prop = mapper.get_property(self.attr.key, resolve_synonyms=True)
|
||||
prop = mapper._props[self.attr.key]
|
||||
self._criterion = prop.compare(
|
||||
operators.eq,
|
||||
instance,
|
||||
@@ -221,7 +231,7 @@ class AppenderMixin(object):
|
||||
if sess is None:
|
||||
return iter(self.attr._get_collection_history(
|
||||
attributes.instance_state(self.instance),
|
||||
passive=True).added_items)
|
||||
attributes.PASSIVE_NO_INITIALIZE).added_items)
|
||||
else:
|
||||
return iter(self._clone(sess))
|
||||
|
||||
@@ -230,7 +240,8 @@ class AppenderMixin(object):
|
||||
if sess is None:
|
||||
return self.attr._get_collection_history(
|
||||
attributes.instance_state(self.instance),
|
||||
passive=True).added_items.__getitem__(index)
|
||||
attributes.PASSIVE_NO_INITIALIZE).added_items.\
|
||||
__getitem__(index)
|
||||
else:
|
||||
return self._clone(sess).__getitem__(index)
|
||||
|
||||
@@ -239,7 +250,7 @@ class AppenderMixin(object):
|
||||
if sess is None:
|
||||
return len(self.attr._get_collection_history(
|
||||
attributes.instance_state(self.instance),
|
||||
passive=True).added_items)
|
||||
attributes.PASSIVE_NO_INITIALIZE).added_items)
|
||||
else:
|
||||
return self._clone(sess).count()
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# orm/evaluator.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
# orm/exc.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 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
|
||||
@@ -7,7 +7,7 @@
|
||||
"""SQLAlchemy ORM exceptions."""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
orm_util = sa.util.importlater('sqlalchemy.orm', 'util')
|
||||
|
||||
NO_STATE = (AttributeError, KeyError)
|
||||
"""Exception types that may be raised by instrumentation implementations."""
|
||||
@@ -15,7 +15,7 @@ NO_STATE = (AttributeError, KeyError)
|
||||
class StaleDataError(sa.exc.SQLAlchemyError):
|
||||
"""An operation encountered database state that is unaccounted for.
|
||||
|
||||
Two conditions cause this to happen:
|
||||
Conditions which cause this to happen include:
|
||||
|
||||
* A flush may have attempted to update or delete rows
|
||||
and an unexpected number of rows were matched during
|
||||
@@ -27,6 +27,12 @@ class StaleDataError(sa.exc.SQLAlchemyError):
|
||||
* A mapped object with version_id_col was refreshed,
|
||||
and the version number coming back from the database does
|
||||
not match that of the object itself.
|
||||
|
||||
* A object is detached from its parent object, however
|
||||
the object was previously attached to a different parent
|
||||
identity which was garbage collected, and a decision
|
||||
cannot be made if the new parent was really the most
|
||||
recent "parent" (new in 0.7.4).
|
||||
|
||||
"""
|
||||
|
||||
@@ -40,6 +46,9 @@ class FlushError(sa.exc.SQLAlchemyError):
|
||||
class UnmappedError(sa.exc.InvalidRequestError):
|
||||
"""Base for exceptions that involve expected mappings not present."""
|
||||
|
||||
class ObjectDereferencedError(sa.exc.SQLAlchemyError):
|
||||
"""An operation cannot complete due to an object being garbage collected."""
|
||||
|
||||
class DetachedInstanceError(sa.exc.SQLAlchemyError):
|
||||
"""An attempt to access unloaded attributes on a
|
||||
mapped instance that is detached."""
|
||||
@@ -63,6 +72,8 @@ class UnmappedInstanceError(UnmappedError):
|
||||
'required?' % _safe_cls_name(obj))
|
||||
UnmappedError.__init__(self, msg)
|
||||
|
||||
def __reduce__(self):
|
||||
return self.__class__, (None, self.args[0])
|
||||
|
||||
class UnmappedClassError(UnmappedError):
|
||||
"""An mapping operation was requested for an unknown class."""
|
||||
@@ -72,10 +83,37 @@ class UnmappedClassError(UnmappedError):
|
||||
msg = _default_unmapped(cls)
|
||||
UnmappedError.__init__(self, msg)
|
||||
|
||||
def __reduce__(self):
|
||||
return self.__class__, (None, self.args[0])
|
||||
|
||||
class ObjectDeletedError(sa.exc.InvalidRequestError):
|
||||
"""An refresh() operation failed to re-retrieve an object's row."""
|
||||
"""A refresh operation failed to retrieve the database
|
||||
row corresponding to an object's known primary key identity.
|
||||
|
||||
A refresh operation proceeds when an expired attribute is
|
||||
accessed on an object, or when :meth:`.Query.get` is
|
||||
used to retrieve an object which is, upon retrieval, detected
|
||||
as expired. A SELECT is emitted for the target row
|
||||
based on primary key; if no row is returned, this
|
||||
exception is raised.
|
||||
|
||||
The true meaning of this exception is simply that
|
||||
no row exists for the primary key identifier associated
|
||||
with a persistent object. The row may have been
|
||||
deleted, or in some cases the primary key updated
|
||||
to a new value, outside of the ORM's management of the target
|
||||
object.
|
||||
|
||||
"""
|
||||
def __init__(self, state, msg=None):
|
||||
if not msg:
|
||||
msg = "Instance '%s' has been deleted, or its "\
|
||||
"row is otherwise not present." % orm_util.state_str(state)
|
||||
|
||||
sa.exc.InvalidRequestError.__init__(self, msg)
|
||||
|
||||
def __reduce__(self):
|
||||
return self.__class__, (None, self.args[0])
|
||||
|
||||
class UnmappedColumnError(sa.exc.InvalidRequestError):
|
||||
"""Mapping operation was requested on an unknown column."""
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
# orm/identity.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 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
|
||||
|
||||
import weakref
|
||||
|
||||
from sqlalchemy import util as base_util
|
||||
from sqlalchemy.orm import attributes
|
||||
|
||||
|
||||
@@ -22,9 +20,6 @@ class IdentityMap(dict):
|
||||
def add(self, state):
|
||||
raise NotImplementedError()
|
||||
|
||||
def remove(self, state):
|
||||
raise NotImplementedError()
|
||||
|
||||
def update(self, dict):
|
||||
raise NotImplementedError("IdentityMap uses add() to insert data")
|
||||
|
||||
@@ -83,7 +78,6 @@ class IdentityMap(dict):
|
||||
class WeakInstanceDict(IdentityMap):
|
||||
def __init__(self):
|
||||
IdentityMap.__init__(self)
|
||||
self._remove_mutex = base_util.threading.Lock()
|
||||
|
||||
def __getitem__(self, key):
|
||||
state = dict.__getitem__(self, key)
|
||||
@@ -123,33 +117,25 @@ class WeakInstanceDict(IdentityMap):
|
||||
self._manage_incoming_state(state)
|
||||
|
||||
def add(self, state):
|
||||
if state.key in self:
|
||||
if dict.__getitem__(self, state.key) is not state:
|
||||
raise AssertionError("A conflicting state is already "
|
||||
"present in the identity map for key %r"
|
||||
% (state.key, ))
|
||||
else:
|
||||
dict.__setitem__(self, state.key, state)
|
||||
self._manage_incoming_state(state)
|
||||
|
||||
def remove_key(self, key):
|
||||
state = dict.__getitem__(self, key)
|
||||
self.remove(state)
|
||||
|
||||
def remove(self, state):
|
||||
self._remove_mutex.acquire()
|
||||
try:
|
||||
if dict.pop(self, state.key) is not state:
|
||||
raise AssertionError("State %s is not present in this identity map" % state)
|
||||
finally:
|
||||
self._remove_mutex.release()
|
||||
|
||||
self._manage_removed_state(state)
|
||||
|
||||
def discard(self, state):
|
||||
if self.contains_state(state):
|
||||
dict.__delitem__(self, state.key)
|
||||
self._manage_removed_state(state)
|
||||
key = state.key
|
||||
# inline of self.__contains__
|
||||
if dict.__contains__(self, key):
|
||||
try:
|
||||
existing_state = dict.__getitem__(self, key)
|
||||
if existing_state is not state:
|
||||
o = existing_state.obj()
|
||||
if o is None:
|
||||
o = existing_state._is_really_none()
|
||||
if o is not None:
|
||||
raise AssertionError("A conflicting state is already "
|
||||
"present in the identity map for key %r"
|
||||
% (key, ))
|
||||
else:
|
||||
return
|
||||
except KeyError:
|
||||
pass
|
||||
dict.__setitem__(self, key, state)
|
||||
self._manage_incoming_state(state)
|
||||
|
||||
def get(self, key, default=None):
|
||||
state = dict.get(self, key, default)
|
||||
@@ -158,58 +144,57 @@ class WeakInstanceDict(IdentityMap):
|
||||
o = state.obj()
|
||||
if o is None:
|
||||
o = state._is_really_none()
|
||||
if o is None:
|
||||
return default
|
||||
if o is None:
|
||||
return default
|
||||
return o
|
||||
|
||||
def _items(self):
|
||||
values = self.all_states()
|
||||
result = []
|
||||
for state in values:
|
||||
value = state.obj()
|
||||
if value is not None:
|
||||
result.append((state.key, value))
|
||||
return result
|
||||
|
||||
def items(self):
|
||||
def _values(self):
|
||||
values = self.all_states()
|
||||
result = []
|
||||
for state in values:
|
||||
value = state.obj()
|
||||
if value is not None:
|
||||
result.append(value)
|
||||
|
||||
return result
|
||||
|
||||
# Py3K
|
||||
#def items(self):
|
||||
# return iter(self._items())
|
||||
#
|
||||
#def values(self):
|
||||
# return iter(self._values())
|
||||
# Py2K
|
||||
return list(self.iteritems())
|
||||
|
||||
items = _items
|
||||
def iteritems(self):
|
||||
# end Py2K
|
||||
self._remove_mutex.acquire()
|
||||
try:
|
||||
result = []
|
||||
for state in dict.values(self):
|
||||
value = state.obj()
|
||||
if value is not None:
|
||||
result.append((state.key, value))
|
||||
|
||||
return iter(result)
|
||||
finally:
|
||||
self._remove_mutex.release()
|
||||
|
||||
def values(self):
|
||||
# Py2K
|
||||
return list(self.itervalues())
|
||||
return iter(self.items())
|
||||
|
||||
values = _values
|
||||
def itervalues(self):
|
||||
return iter(self.values())
|
||||
# end Py2K
|
||||
self._remove_mutex.acquire()
|
||||
try:
|
||||
result = []
|
||||
for state in dict.values(self):
|
||||
value = state.obj()
|
||||
if value is not None:
|
||||
result.append(value)
|
||||
|
||||
return iter(result)
|
||||
finally:
|
||||
self._remove_mutex.release()
|
||||
|
||||
def all_states(self):
|
||||
self._remove_mutex.acquire()
|
||||
try:
|
||||
# Py3K
|
||||
# return list(dict.values(self))
|
||||
# Py3K
|
||||
# return list(dict.values(self))
|
||||
# Py2K
|
||||
return dict.values(self)
|
||||
# end Py2K
|
||||
|
||||
# Py2K
|
||||
return dict.values(self)
|
||||
# end Py2K
|
||||
finally:
|
||||
self._remove_mutex.release()
|
||||
def discard(self, state):
|
||||
st = dict.get(self, state.key, None)
|
||||
if st is state:
|
||||
dict.pop(self, state.key, None)
|
||||
self._manage_removed_state(state)
|
||||
|
||||
def prune(self):
|
||||
return 0
|
||||
@@ -235,25 +220,22 @@ class StrongInstanceDict(IdentityMap):
|
||||
|
||||
def add(self, state):
|
||||
if state.key in self:
|
||||
if attributes.instance_state(dict.__getitem__(self, state.key)) is not state:
|
||||
raise AssertionError("A conflicting state is already present in the identity map for key %r" % (state.key, ))
|
||||
if attributes.instance_state(dict.__getitem__(self,
|
||||
state.key)) is not state:
|
||||
raise AssertionError('A conflicting state is already '
|
||||
'present in the identity map for key %r'
|
||||
% (state.key, ))
|
||||
else:
|
||||
dict.__setitem__(self, state.key, state.obj())
|
||||
self._manage_incoming_state(state)
|
||||
|
||||
def remove(self, state):
|
||||
if attributes.instance_state(dict.pop(self, state.key)) is not state:
|
||||
raise AssertionError("State %s is not present in this identity map" % state)
|
||||
self._manage_removed_state(state)
|
||||
|
||||
def discard(self, state):
|
||||
if self.contains_state(state):
|
||||
dict.__delitem__(self, state.key)
|
||||
self._manage_removed_state(state)
|
||||
|
||||
def remove_key(self, key):
|
||||
state = attributes.instance_state(dict.__getitem__(self, key))
|
||||
self.remove(state)
|
||||
obj = dict.get(self, state.key, None)
|
||||
if obj is not None:
|
||||
st = attributes.instance_state(obj)
|
||||
if st is state:
|
||||
dict.pop(self, state.key, None)
|
||||
self._manage_removed_state(state)
|
||||
|
||||
def prune(self):
|
||||
"""prune unreferenced, non-dirty states."""
|
||||
|
||||
@@ -0,0 +1,674 @@
|
||||
# orm/instrumentation.py
|
||||
# Copyright (C) 2005-2012 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
|
||||
|
||||
"""Defines SQLAlchemy's system of class instrumentation.
|
||||
|
||||
This module is usually not directly visible to user applications, but
|
||||
defines a large part of the ORM's interactivity.
|
||||
|
||||
instrumentation.py deals with registration of end-user classes
|
||||
for state tracking. It interacts closely with state.py
|
||||
and attributes.py which establish per-instance and per-class-attribute
|
||||
instrumentation, respectively.
|
||||
|
||||
SQLA's instrumentation system is completely customizable, in which
|
||||
case an understanding of the general mechanics of this module is helpful.
|
||||
An example of full customization is in /examples/custom_attributes.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
from sqlalchemy.orm import exc, collections, events
|
||||
from operator import attrgetter, itemgetter
|
||||
from sqlalchemy import event, util
|
||||
import weakref
|
||||
from sqlalchemy.orm import state, attributes
|
||||
|
||||
|
||||
INSTRUMENTATION_MANAGER = '__sa_instrumentation_manager__'
|
||||
"""Attribute, elects custom instrumentation when present on a mapped class.
|
||||
|
||||
Allows a class to specify a slightly or wildly different technique for
|
||||
tracking changes made to mapped attributes and collections.
|
||||
|
||||
Only one instrumentation implementation is allowed in a given object
|
||||
inheritance hierarchy.
|
||||
|
||||
The value of this attribute must be a callable and will be passed a class
|
||||
object. The callable must return one of:
|
||||
|
||||
- An instance of an interfaces.InstrumentationManager or subclass
|
||||
- An object implementing all or some of InstrumentationManager (TODO)
|
||||
- A dictionary of callables, implementing all or some of the above (TODO)
|
||||
- An instance of a ClassManager or subclass
|
||||
|
||||
interfaces.InstrumentationManager is public API and will remain stable
|
||||
between releases. ClassManager is not public and no guarantees are made
|
||||
about stability. Caveat emptor.
|
||||
|
||||
This attribute is consulted by the default SQLAlchemy instrumentation
|
||||
resolution code. If custom finders are installed in the global
|
||||
instrumentation_finders list, they may or may not choose to honor this
|
||||
attribute.
|
||||
|
||||
"""
|
||||
|
||||
instrumentation_finders = []
|
||||
"""An extensible sequence of instrumentation implementation finding callables.
|
||||
|
||||
Finders callables will be passed a class object. If None is returned, the
|
||||
next finder in the sequence is consulted. Otherwise the return must be an
|
||||
instrumentation factory that follows the same guidelines as
|
||||
INSTRUMENTATION_MANAGER.
|
||||
|
||||
By default, the only finder is find_native_user_instrumentation_hook, which
|
||||
searches for INSTRUMENTATION_MANAGER. If all finders return None, standard
|
||||
ClassManager instrumentation is used.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class ClassManager(dict):
|
||||
"""tracks state information at the class level."""
|
||||
|
||||
MANAGER_ATTR = '_sa_class_manager'
|
||||
STATE_ATTR = '_sa_instance_state'
|
||||
|
||||
deferred_scalar_loader = None
|
||||
|
||||
original_init = object.__init__
|
||||
|
||||
def __init__(self, class_):
|
||||
self.class_ = class_
|
||||
self.factory = None # where we came from, for inheritance bookkeeping
|
||||
self.info = {}
|
||||
self.new_init = None
|
||||
self.mutable_attributes = set()
|
||||
self.local_attrs = {}
|
||||
self.originals = {}
|
||||
|
||||
self._bases = [mgr for mgr in [
|
||||
manager_of_class(base)
|
||||
for base in self.class_.__bases__
|
||||
if isinstance(base, type)
|
||||
] if mgr is not None]
|
||||
|
||||
for base in self._bases:
|
||||
self.update(base)
|
||||
|
||||
self.manage()
|
||||
self._instrument_init()
|
||||
|
||||
dispatch = event.dispatcher(events.InstanceEvents)
|
||||
|
||||
@property
|
||||
def is_mapped(self):
|
||||
return 'mapper' in self.__dict__
|
||||
|
||||
@util.memoized_property
|
||||
def mapper(self):
|
||||
# raises unless self.mapper has been assigned
|
||||
raise exc.UnmappedClassError(self.class_)
|
||||
|
||||
def _attr_has_impl(self, key):
|
||||
"""Return True if the given attribute is fully initialized.
|
||||
|
||||
i.e. has an impl.
|
||||
"""
|
||||
|
||||
return key in self and self[key].impl is not None
|
||||
|
||||
def _subclass_manager(self, cls):
|
||||
"""Create a new ClassManager for a subclass of this ClassManager's
|
||||
class.
|
||||
|
||||
This is called automatically when attributes are instrumented so that
|
||||
the attributes can be propagated to subclasses against their own
|
||||
class-local manager, without the need for mappers etc. to have already
|
||||
pre-configured managers for the full class hierarchy. Mappers
|
||||
can post-configure the auto-generated ClassManager when needed.
|
||||
|
||||
"""
|
||||
manager = manager_of_class(cls)
|
||||
if manager is None:
|
||||
manager = _create_manager_for_cls(cls, _source=self)
|
||||
return manager
|
||||
|
||||
def _instrument_init(self):
|
||||
# TODO: self.class_.__init__ is often the already-instrumented
|
||||
# __init__ from an instrumented superclass. We still need to make
|
||||
# our own wrapper, but it would
|
||||
# be nice to wrap the original __init__ and not our existing wrapper
|
||||
# of such, since this adds method overhead.
|
||||
self.original_init = self.class_.__init__
|
||||
self.new_init = _generate_init(self.class_, self)
|
||||
self.install_member('__init__', self.new_init)
|
||||
|
||||
def _uninstrument_init(self):
|
||||
if self.new_init:
|
||||
self.uninstall_member('__init__')
|
||||
self.new_init = None
|
||||
|
||||
@util.memoized_property
|
||||
def _state_constructor(self):
|
||||
self.dispatch.first_init(self, self.class_)
|
||||
if self.mutable_attributes:
|
||||
return state.MutableAttrInstanceState
|
||||
else:
|
||||
return state.InstanceState
|
||||
|
||||
def manage(self):
|
||||
"""Mark this instance as the manager for its class."""
|
||||
|
||||
setattr(self.class_, self.MANAGER_ATTR, self)
|
||||
|
||||
def dispose(self):
|
||||
"""Dissasociate this manager from its class."""
|
||||
|
||||
delattr(self.class_, self.MANAGER_ATTR)
|
||||
|
||||
def manager_getter(self):
|
||||
return attrgetter(self.MANAGER_ATTR)
|
||||
|
||||
def instrument_attribute(self, key, inst, propagated=False):
|
||||
if propagated:
|
||||
if key in self.local_attrs:
|
||||
return # don't override local attr with inherited attr
|
||||
else:
|
||||
self.local_attrs[key] = inst
|
||||
self.install_descriptor(key, inst)
|
||||
self[key] = inst
|
||||
|
||||
for cls in self.class_.__subclasses__():
|
||||
manager = self._subclass_manager(cls)
|
||||
manager.instrument_attribute(key, inst, True)
|
||||
|
||||
def subclass_managers(self, recursive):
|
||||
for cls in self.class_.__subclasses__():
|
||||
mgr = manager_of_class(cls)
|
||||
if mgr is not None and mgr is not self:
|
||||
yield mgr
|
||||
if recursive:
|
||||
for m in mgr.subclass_managers(True):
|
||||
yield m
|
||||
|
||||
def post_configure_attribute(self, key):
|
||||
instrumentation_registry.dispatch.\
|
||||
attribute_instrument(self.class_, key, self[key])
|
||||
|
||||
def uninstrument_attribute(self, key, propagated=False):
|
||||
if key not in self:
|
||||
return
|
||||
if propagated:
|
||||
if key in self.local_attrs:
|
||||
return # don't get rid of local attr
|
||||
else:
|
||||
del self.local_attrs[key]
|
||||
self.uninstall_descriptor(key)
|
||||
del self[key]
|
||||
if key in self.mutable_attributes:
|
||||
self.mutable_attributes.remove(key)
|
||||
for cls in self.class_.__subclasses__():
|
||||
manager = manager_of_class(cls)
|
||||
if manager:
|
||||
manager.uninstrument_attribute(key, True)
|
||||
|
||||
def unregister(self):
|
||||
"""remove all instrumentation established by this ClassManager."""
|
||||
|
||||
self._uninstrument_init()
|
||||
|
||||
self.mapper = self.dispatch = None
|
||||
self.info.clear()
|
||||
|
||||
for key in list(self):
|
||||
if key in self.local_attrs:
|
||||
self.uninstrument_attribute(key)
|
||||
|
||||
def install_descriptor(self, key, inst):
|
||||
if key in (self.STATE_ATTR, self.MANAGER_ATTR):
|
||||
raise KeyError("%r: requested attribute name conflicts with "
|
||||
"instrumentation attribute of the same name." %
|
||||
key)
|
||||
setattr(self.class_, key, inst)
|
||||
|
||||
def uninstall_descriptor(self, key):
|
||||
delattr(self.class_, key)
|
||||
|
||||
def install_member(self, key, implementation):
|
||||
if key in (self.STATE_ATTR, self.MANAGER_ATTR):
|
||||
raise KeyError("%r: requested attribute name conflicts with "
|
||||
"instrumentation attribute of the same name." %
|
||||
key)
|
||||
self.originals.setdefault(key, getattr(self.class_, key, None))
|
||||
setattr(self.class_, key, implementation)
|
||||
|
||||
def uninstall_member(self, key):
|
||||
original = self.originals.pop(key, None)
|
||||
if original is not None:
|
||||
setattr(self.class_, key, original)
|
||||
|
||||
def instrument_collection_class(self, key, collection_class):
|
||||
return collections.prepare_instrumentation(collection_class)
|
||||
|
||||
def initialize_collection(self, key, state, factory):
|
||||
user_data = factory()
|
||||
adapter = collections.CollectionAdapter(
|
||||
self.get_impl(key), state, user_data)
|
||||
return adapter, user_data
|
||||
|
||||
def is_instrumented(self, key, search=False):
|
||||
if search:
|
||||
return key in self
|
||||
else:
|
||||
return key in self.local_attrs
|
||||
|
||||
def get_impl(self, key):
|
||||
return self[key].impl
|
||||
|
||||
@property
|
||||
def attributes(self):
|
||||
return self.itervalues()
|
||||
|
||||
## InstanceState management
|
||||
|
||||
def new_instance(self, state=None):
|
||||
instance = self.class_.__new__(self.class_)
|
||||
setattr(instance, self.STATE_ATTR,
|
||||
state or self._state_constructor(instance, self))
|
||||
return instance
|
||||
|
||||
def setup_instance(self, instance, state=None):
|
||||
setattr(instance, self.STATE_ATTR,
|
||||
state or self._state_constructor(instance, self))
|
||||
|
||||
def teardown_instance(self, instance):
|
||||
delattr(instance, self.STATE_ATTR)
|
||||
|
||||
def _new_state_if_none(self, instance):
|
||||
"""Install a default InstanceState if none is present.
|
||||
|
||||
A private convenience method used by the __init__ decorator.
|
||||
|
||||
"""
|
||||
if hasattr(instance, self.STATE_ATTR):
|
||||
return False
|
||||
elif self.class_ is not instance.__class__ and \
|
||||
self.is_mapped:
|
||||
# this will create a new ClassManager for the
|
||||
# subclass, without a mapper. This is likely a
|
||||
# user error situation but allow the object
|
||||
# to be constructed, so that it is usable
|
||||
# in a non-ORM context at least.
|
||||
return self._subclass_manager(instance.__class__).\
|
||||
_new_state_if_none(instance)
|
||||
else:
|
||||
state = self._state_constructor(instance, self)
|
||||
setattr(instance, self.STATE_ATTR, state)
|
||||
return state
|
||||
|
||||
def state_getter(self):
|
||||
"""Return a (instance) -> InstanceState callable.
|
||||
|
||||
"state getter" callables should raise either KeyError or
|
||||
AttributeError if no InstanceState could be found for the
|
||||
instance.
|
||||
"""
|
||||
|
||||
return attrgetter(self.STATE_ATTR)
|
||||
|
||||
def dict_getter(self):
|
||||
return attrgetter('__dict__')
|
||||
|
||||
def has_state(self, instance):
|
||||
return hasattr(instance, self.STATE_ATTR)
|
||||
|
||||
def has_parent(self, state, key, optimistic=False):
|
||||
"""TODO"""
|
||||
return self.get_impl(key).hasparent(state, optimistic=optimistic)
|
||||
|
||||
def __nonzero__(self):
|
||||
"""All ClassManagers are non-zero regardless of attribute state."""
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s of %r at %x>' % (
|
||||
self.__class__.__name__, self.class_, id(self))
|
||||
|
||||
class _ClassInstrumentationAdapter(ClassManager):
|
||||
"""Adapts a user-defined InstrumentationManager to a ClassManager."""
|
||||
|
||||
def __init__(self, class_, override, **kw):
|
||||
self._adapted = override
|
||||
self._get_state = self._adapted.state_getter(class_)
|
||||
self._get_dict = self._adapted.dict_getter(class_)
|
||||
|
||||
ClassManager.__init__(self, class_, **kw)
|
||||
|
||||
def manage(self):
|
||||
self._adapted.manage(self.class_, self)
|
||||
|
||||
def dispose(self):
|
||||
self._adapted.dispose(self.class_)
|
||||
|
||||
def manager_getter(self):
|
||||
return self._adapted.manager_getter(self.class_)
|
||||
|
||||
def instrument_attribute(self, key, inst, propagated=False):
|
||||
ClassManager.instrument_attribute(self, key, inst, propagated)
|
||||
if not propagated:
|
||||
self._adapted.instrument_attribute(self.class_, key, inst)
|
||||
|
||||
def post_configure_attribute(self, key):
|
||||
super(_ClassInstrumentationAdapter, self).post_configure_attribute(key)
|
||||
self._adapted.post_configure_attribute(self.class_, key, self[key])
|
||||
|
||||
def install_descriptor(self, key, inst):
|
||||
self._adapted.install_descriptor(self.class_, key, inst)
|
||||
|
||||
def uninstall_descriptor(self, key):
|
||||
self._adapted.uninstall_descriptor(self.class_, key)
|
||||
|
||||
def install_member(self, key, implementation):
|
||||
self._adapted.install_member(self.class_, key, implementation)
|
||||
|
||||
def uninstall_member(self, key):
|
||||
self._adapted.uninstall_member(self.class_, key)
|
||||
|
||||
def instrument_collection_class(self, key, collection_class):
|
||||
return self._adapted.instrument_collection_class(
|
||||
self.class_, key, collection_class)
|
||||
|
||||
def initialize_collection(self, key, state, factory):
|
||||
delegate = getattr(self._adapted, 'initialize_collection', None)
|
||||
if delegate:
|
||||
return delegate(key, state, factory)
|
||||
else:
|
||||
return ClassManager.initialize_collection(self, key,
|
||||
state, factory)
|
||||
|
||||
def new_instance(self, state=None):
|
||||
instance = self.class_.__new__(self.class_)
|
||||
self.setup_instance(instance, state)
|
||||
return instance
|
||||
|
||||
def _new_state_if_none(self, instance):
|
||||
"""Install a default InstanceState if none is present.
|
||||
|
||||
A private convenience method used by the __init__ decorator.
|
||||
"""
|
||||
if self.has_state(instance):
|
||||
return False
|
||||
else:
|
||||
return self.setup_instance(instance)
|
||||
|
||||
def setup_instance(self, instance, state=None):
|
||||
self._adapted.initialize_instance_dict(self.class_, instance)
|
||||
|
||||
if state is None:
|
||||
state = self._state_constructor(instance, self)
|
||||
|
||||
# the given instance is assumed to have no state
|
||||
self._adapted.install_state(self.class_, instance, state)
|
||||
return state
|
||||
|
||||
def teardown_instance(self, instance):
|
||||
self._adapted.remove_state(self.class_, instance)
|
||||
|
||||
def has_state(self, instance):
|
||||
try:
|
||||
state = self._get_state(instance)
|
||||
except exc.NO_STATE:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def state_getter(self):
|
||||
return self._get_state
|
||||
|
||||
def dict_getter(self):
|
||||
return self._get_dict
|
||||
|
||||
def register_class(class_, **kw):
|
||||
"""Register class instrumentation.
|
||||
|
||||
Returns the existing or newly created class manager.
|
||||
"""
|
||||
|
||||
manager = manager_of_class(class_)
|
||||
if manager is None:
|
||||
manager = _create_manager_for_cls(class_, **kw)
|
||||
return manager
|
||||
|
||||
def unregister_class(class_):
|
||||
"""Unregister class instrumentation."""
|
||||
|
||||
instrumentation_registry.unregister(class_)
|
||||
|
||||
|
||||
def is_instrumented(instance, key):
|
||||
"""Return True if the given attribute on the given instance is
|
||||
instrumented by the attributes package.
|
||||
|
||||
This function may be used regardless of instrumentation
|
||||
applied directly to the class, i.e. no descriptors are required.
|
||||
|
||||
"""
|
||||
return manager_of_class(instance.__class__).\
|
||||
is_instrumented(key, search=True)
|
||||
|
||||
class InstrumentationRegistry(object):
|
||||
"""Private instrumentation registration singleton.
|
||||
|
||||
All classes are routed through this registry
|
||||
when first instrumented, however the InstrumentationRegistry
|
||||
is not actually needed unless custom ClassManagers are in use.
|
||||
|
||||
"""
|
||||
|
||||
_manager_finders = weakref.WeakKeyDictionary()
|
||||
_state_finders = util.WeakIdentityMapping()
|
||||
_dict_finders = util.WeakIdentityMapping()
|
||||
_extended = False
|
||||
|
||||
dispatch = event.dispatcher(events.InstrumentationEvents)
|
||||
|
||||
def create_manager_for_cls(self, class_, **kw):
|
||||
assert class_ is not None
|
||||
assert manager_of_class(class_) is None
|
||||
|
||||
for finder in instrumentation_finders:
|
||||
factory = finder(class_)
|
||||
if factory is not None:
|
||||
break
|
||||
else:
|
||||
factory = ClassManager
|
||||
|
||||
existing_factories = self._collect_management_factories_for(class_).\
|
||||
difference([factory])
|
||||
if existing_factories:
|
||||
raise TypeError(
|
||||
"multiple instrumentation implementations specified "
|
||||
"in %s inheritance hierarchy: %r" % (
|
||||
class_.__name__, list(existing_factories)))
|
||||
|
||||
manager = factory(class_)
|
||||
if not isinstance(manager, ClassManager):
|
||||
manager = _ClassInstrumentationAdapter(class_, manager)
|
||||
|
||||
if factory != ClassManager and not self._extended:
|
||||
# somebody invoked a custom ClassManager.
|
||||
# reinstall global "getter" functions with the more
|
||||
# expensive ones.
|
||||
self._extended = True
|
||||
_install_lookup_strategy(self)
|
||||
|
||||
manager.factory = factory
|
||||
self._manager_finders[class_] = manager.manager_getter()
|
||||
self._state_finders[class_] = manager.state_getter()
|
||||
self._dict_finders[class_] = manager.dict_getter()
|
||||
|
||||
self.dispatch.class_instrument(class_)
|
||||
|
||||
return manager
|
||||
|
||||
def _collect_management_factories_for(self, cls):
|
||||
"""Return a collection of factories in play or specified for a
|
||||
hierarchy.
|
||||
|
||||
Traverses the entire inheritance graph of a cls and returns a
|
||||
collection of instrumentation factories for those classes. Factories
|
||||
are extracted from active ClassManagers, if available, otherwise
|
||||
instrumentation_finders is consulted.
|
||||
|
||||
"""
|
||||
hierarchy = util.class_hierarchy(cls)
|
||||
factories = set()
|
||||
for member in hierarchy:
|
||||
manager = manager_of_class(member)
|
||||
if manager is not None:
|
||||
factories.add(manager.factory)
|
||||
else:
|
||||
for finder in instrumentation_finders:
|
||||
factory = finder(member)
|
||||
if factory is not None:
|
||||
break
|
||||
else:
|
||||
factory = None
|
||||
factories.add(factory)
|
||||
factories.discard(None)
|
||||
return factories
|
||||
|
||||
def manager_of_class(self, cls):
|
||||
# this is only called when alternate instrumentation
|
||||
# has been established
|
||||
if cls is None:
|
||||
return None
|
||||
try:
|
||||
finder = self._manager_finders[cls]
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return finder(cls)
|
||||
|
||||
def state_of(self, instance):
|
||||
# this is only called when alternate instrumentation
|
||||
# has been established
|
||||
if instance is None:
|
||||
raise AttributeError("None has no persistent state.")
|
||||
try:
|
||||
return self._state_finders[instance.__class__](instance)
|
||||
except KeyError:
|
||||
raise AttributeError("%r is not instrumented" %
|
||||
instance.__class__)
|
||||
|
||||
def dict_of(self, instance):
|
||||
# this is only called when alternate instrumentation
|
||||
# has been established
|
||||
if instance is None:
|
||||
raise AttributeError("None has no persistent state.")
|
||||
try:
|
||||
return self._dict_finders[instance.__class__](instance)
|
||||
except KeyError:
|
||||
raise AttributeError("%r is not instrumented" %
|
||||
instance.__class__)
|
||||
|
||||
def unregister(self, class_):
|
||||
if class_ in self._manager_finders:
|
||||
manager = self.manager_of_class(class_)
|
||||
self.dispatch.class_uninstrument(class_)
|
||||
manager.unregister()
|
||||
manager.dispose()
|
||||
del self._manager_finders[class_]
|
||||
del self._state_finders[class_]
|
||||
del self._dict_finders[class_]
|
||||
if ClassManager.MANAGER_ATTR in class_.__dict__:
|
||||
delattr(class_, ClassManager.MANAGER_ATTR)
|
||||
|
||||
instrumentation_registry = InstrumentationRegistry()
|
||||
|
||||
|
||||
def _install_lookup_strategy(implementation):
|
||||
"""Replace global class/object management functions
|
||||
with either faster or more comprehensive implementations,
|
||||
based on whether or not extended class instrumentation
|
||||
has been detected.
|
||||
|
||||
This function is called only by InstrumentationRegistry()
|
||||
and unit tests specific to this behavior.
|
||||
|
||||
"""
|
||||
global instance_state, instance_dict, manager_of_class
|
||||
if implementation is util.symbol('native'):
|
||||
instance_state = attrgetter(ClassManager.STATE_ATTR)
|
||||
instance_dict = attrgetter("__dict__")
|
||||
def manager_of_class(cls):
|
||||
return cls.__dict__.get(ClassManager.MANAGER_ATTR, None)
|
||||
else:
|
||||
instance_state = instrumentation_registry.state_of
|
||||
instance_dict = instrumentation_registry.dict_of
|
||||
manager_of_class = instrumentation_registry.manager_of_class
|
||||
attributes.instance_state = instance_state
|
||||
attributes.instance_dict = instance_dict
|
||||
attributes.manager_of_class = manager_of_class
|
||||
|
||||
_create_manager_for_cls = instrumentation_registry.create_manager_for_cls
|
||||
|
||||
# Install default "lookup" strategies. These are basically
|
||||
# very fast attrgetters for key attributes.
|
||||
# When a custom ClassManager is installed, more expensive per-class
|
||||
# strategies are copied over these.
|
||||
_install_lookup_strategy(util.symbol('native'))
|
||||
|
||||
|
||||
def find_native_user_instrumentation_hook(cls):
|
||||
"""Find user-specified instrumentation management for a class."""
|
||||
return getattr(cls, INSTRUMENTATION_MANAGER, None)
|
||||
instrumentation_finders.append(find_native_user_instrumentation_hook)
|
||||
|
||||
def _generate_init(class_, class_manager):
|
||||
"""Build an __init__ decorator that triggers ClassManager events."""
|
||||
|
||||
# TODO: we should use the ClassManager's notion of the
|
||||
# original '__init__' method, once ClassManager is fixed
|
||||
# to always reference that.
|
||||
original__init__ = class_.__init__
|
||||
assert original__init__
|
||||
|
||||
# Go through some effort here and don't change the user's __init__
|
||||
# calling signature, including the unlikely case that it has
|
||||
# a return value.
|
||||
# FIXME: need to juggle local names to avoid constructor argument
|
||||
# clashes.
|
||||
func_body = """\
|
||||
def __init__(%(apply_pos)s):
|
||||
new_state = class_manager._new_state_if_none(%(self_arg)s)
|
||||
if new_state:
|
||||
return new_state.initialize_instance(%(apply_kw)s)
|
||||
else:
|
||||
return original__init__(%(apply_kw)s)
|
||||
"""
|
||||
func_vars = util.format_argspec_init(original__init__, grouped=False)
|
||||
func_text = func_body % func_vars
|
||||
|
||||
# Py3K
|
||||
#func_defaults = getattr(original__init__, '__defaults__', None)
|
||||
#func_kw_defaults = getattr(original__init__, '__kwdefaults__', None)
|
||||
# Py2K
|
||||
func = getattr(original__init__, 'im_func', original__init__)
|
||||
func_defaults = getattr(func, 'func_defaults', None)
|
||||
# end Py2K
|
||||
|
||||
env = locals().copy()
|
||||
exec func_text in env
|
||||
__init__ = env['__init__']
|
||||
__init__.__doc__ = original__init__.__doc__
|
||||
if func_defaults:
|
||||
__init__.func_defaults = func_defaults
|
||||
# Py3K
|
||||
#if func_kw_defaults:
|
||||
# __init__.__kwdefaults__ = func_kw_defaults
|
||||
return __init__
|
||||
+216
-527
@@ -1,26 +1,30 @@
|
||||
# orm/interfaces.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 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
|
||||
|
||||
"""
|
||||
|
||||
Semi-private module containing various base classes used throughout the ORM.
|
||||
Contains various base classes used throughout the ORM.
|
||||
|
||||
Defines the extension classes :class:`MapperExtension`,
|
||||
:class:`SessionExtension`, and :class:`AttributeExtension` as
|
||||
well as other user-subclassable extension objects.
|
||||
Defines the now deprecated ORM extension classes as well
|
||||
as ORM internals.
|
||||
|
||||
Other than the deprecated extensions, this module and the
|
||||
classes within should be considered mostly private.
|
||||
|
||||
"""
|
||||
|
||||
from itertools import chain
|
||||
|
||||
import sqlalchemy.exceptions as sa_exc
|
||||
from sqlalchemy import log, util
|
||||
from sqlalchemy.sql import expression
|
||||
from sqlalchemy import exc as sa_exc
|
||||
from sqlalchemy import util
|
||||
from sqlalchemy.sql import operators
|
||||
deque = __import__('collections').deque
|
||||
|
||||
mapperutil = util.importlater('sqlalchemy.orm', 'util')
|
||||
|
||||
class_mapper = None
|
||||
collections = None
|
||||
|
||||
__all__ = (
|
||||
@@ -48,373 +52,22 @@ ONETOMANY = util.symbol('ONETOMANY')
|
||||
MANYTOONE = util.symbol('MANYTOONE')
|
||||
MANYTOMANY = util.symbol('MANYTOMANY')
|
||||
|
||||
class MapperExtension(object):
|
||||
"""Base implementation for customizing ``Mapper`` behavior.
|
||||
from deprecated_interfaces import AttributeExtension, SessionExtension, \
|
||||
MapperExtension
|
||||
|
||||
New extension classes subclass ``MapperExtension`` and are specified
|
||||
using the ``extension`` mapper() argument, which is a single
|
||||
``MapperExtension`` or a list of such. A single mapper
|
||||
can maintain a chain of ``MapperExtension`` objects. When a
|
||||
particular mapping event occurs, the corresponding method
|
||||
on each ``MapperExtension`` is invoked serially, and each method
|
||||
has the ability to halt the chain from proceeding further.
|
||||
|
||||
Each ``MapperExtension`` method returns the symbol
|
||||
EXT_CONTINUE by default. This symbol generally means "move
|
||||
to the next ``MapperExtension`` for processing". For methods
|
||||
that return objects like translated rows or new object
|
||||
instances, EXT_CONTINUE means the result of the method
|
||||
should be ignored. In some cases it's required for a
|
||||
default mapper activity to be performed, such as adding a
|
||||
new instance to a result list.
|
||||
|
||||
The symbol EXT_STOP has significance within a chain
|
||||
of ``MapperExtension`` objects that the chain will be stopped
|
||||
when this symbol is returned. Like EXT_CONTINUE, it also
|
||||
has additional significance in some cases that a default
|
||||
mapper activity will not be performed.
|
||||
|
||||
"""
|
||||
|
||||
def instrument_class(self, mapper, class_):
|
||||
"""Receive a class when the mapper is first constructed, and has
|
||||
applied instrumentation to the mapped class.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def init_instance(self, mapper, class_, oldinit, instance, args, kwargs):
|
||||
"""Receive an instance when it's constructor is called.
|
||||
|
||||
This method is only called during a userland construction of
|
||||
an object. It is not called when an object is loaded from the
|
||||
database.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def init_failed(self, mapper, class_, oldinit, instance, args, kwargs):
|
||||
"""Receive an instance when it's constructor has been called,
|
||||
and raised an exception.
|
||||
|
||||
This method is only called during a userland construction of
|
||||
an object. It is not called when an object is loaded from the
|
||||
database.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def translate_row(self, mapper, context, row):
|
||||
"""Perform pre-processing on the given result row and return a
|
||||
new row instance.
|
||||
|
||||
This is called when the mapper first receives a row, before
|
||||
the object identity or the instance itself has been derived
|
||||
from that row. The given row may or may not be a
|
||||
``RowProxy`` object - it will always be a dictionary-like
|
||||
object which contains mapped columns as keys. The
|
||||
returned object should also be a dictionary-like object
|
||||
which recognizes mapped columns as keys.
|
||||
|
||||
If the ultimate return value is EXT_CONTINUE, the row
|
||||
is not translated.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def create_instance(self, mapper, selectcontext, row, class_):
|
||||
"""Receive a row when a new object instance is about to be
|
||||
created from that row.
|
||||
|
||||
The method can choose to create the instance itself, or it can return
|
||||
EXT_CONTINUE to indicate normal object creation should take place.
|
||||
|
||||
mapper
|
||||
The mapper doing the operation
|
||||
|
||||
selectcontext
|
||||
The QueryContext generated from the Query.
|
||||
|
||||
row
|
||||
The result row from the database
|
||||
|
||||
class\_
|
||||
The class we are mapping.
|
||||
|
||||
return value
|
||||
A new object instance, or EXT_CONTINUE
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def append_result(self, mapper, selectcontext, row, instance,
|
||||
result, **flags):
|
||||
"""Receive an object instance before that instance is appended
|
||||
to a result list.
|
||||
|
||||
If this method returns EXT_CONTINUE, result appending will proceed
|
||||
normally. if this method returns any other value or None,
|
||||
result appending will not proceed for this instance, giving
|
||||
this extension an opportunity to do the appending itself, if
|
||||
desired.
|
||||
|
||||
mapper
|
||||
The mapper doing the operation.
|
||||
|
||||
selectcontext
|
||||
The QueryContext generated from the Query.
|
||||
|
||||
row
|
||||
The result row from the database.
|
||||
|
||||
instance
|
||||
The object instance to be appended to the result.
|
||||
|
||||
result
|
||||
List to which results are being appended.
|
||||
|
||||
\**flags
|
||||
extra information about the row, same as criterion in
|
||||
``create_row_processor()`` method of
|
||||
:class:`~sqlalchemy.orm.interfaces.MapperProperty`
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def populate_instance(self, mapper, selectcontext, row,
|
||||
instance, **flags):
|
||||
"""Receive an instance before that instance has
|
||||
its attributes populated.
|
||||
|
||||
This usually corresponds to a newly loaded instance but may
|
||||
also correspond to an already-loaded instance which has
|
||||
unloaded attributes to be populated. The method may be called
|
||||
many times for a single instance, as multiple result rows are
|
||||
used to populate eagerly loaded collections.
|
||||
|
||||
If this method returns EXT_CONTINUE, instance population will
|
||||
proceed normally. If any other value or None is returned,
|
||||
instance population will not proceed, giving this extension an
|
||||
opportunity to populate the instance itself, if desired.
|
||||
|
||||
As of 0.5, most usages of this hook are obsolete. For a
|
||||
generic "object has been newly created from a row" hook, use
|
||||
``reconstruct_instance()``, or the ``@orm.reconstructor``
|
||||
decorator.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def reconstruct_instance(self, mapper, instance):
|
||||
"""Receive an object instance after it has been created via
|
||||
``__new__``, and after initial attribute population has
|
||||
occurred.
|
||||
|
||||
This typically occurs when the instance is created based on
|
||||
incoming result rows, and is only called once for that
|
||||
instance's lifetime.
|
||||
|
||||
Note that during a result-row load, this method is called upon
|
||||
the first row received for this instance. Note that some
|
||||
attributes and collections may or may not be loaded or even
|
||||
initialized, depending on what's present in the result rows.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def before_insert(self, mapper, connection, instance):
|
||||
"""Receive an object instance before that instance is inserted
|
||||
into its table.
|
||||
|
||||
This is a good place to set up primary key values and such
|
||||
that aren't handled otherwise.
|
||||
|
||||
Column-based attributes can be modified within this method
|
||||
which will result in the new value being inserted. However
|
||||
*no* changes to the overall flush plan can be made, and
|
||||
manipulation of the ``Session`` will not have the desired effect.
|
||||
To manipulate the ``Session`` within an extension, use
|
||||
``SessionExtension``.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def after_insert(self, mapper, connection, instance):
|
||||
"""Receive an object instance after that instance is inserted.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def before_update(self, mapper, connection, instance):
|
||||
"""Receive an object instance before that instance is updated.
|
||||
|
||||
Note that this method is called for all instances that are marked as
|
||||
"dirty", even those which have no net changes to their column-based
|
||||
attributes. An object is marked as dirty when any of its column-based
|
||||
attributes have a "set attribute" operation called or when any of its
|
||||
collections are modified. If, at update time, no column-based
|
||||
attributes have any net changes, no UPDATE statement will be issued.
|
||||
This means that an instance being sent to before_update is *not* a
|
||||
guarantee that an UPDATE statement will be issued (although you can
|
||||
affect the outcome here).
|
||||
|
||||
To detect if the column-based attributes on the object have net
|
||||
changes, and will therefore generate an UPDATE statement, use
|
||||
``object_session(instance).is_modified(instance,
|
||||
include_collections=False)``.
|
||||
|
||||
Column-based attributes can be modified within this method
|
||||
which will result in the new value being updated. However
|
||||
*no* changes to the overall flush plan can be made, and
|
||||
manipulation of the ``Session`` will not have the desired effect.
|
||||
To manipulate the ``Session`` within an extension, use
|
||||
``SessionExtension``.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def after_update(self, mapper, connection, instance):
|
||||
"""Receive an object instance after that instance is updated.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def before_delete(self, mapper, connection, instance):
|
||||
"""Receive an object instance before that instance is deleted.
|
||||
|
||||
Note that *no* changes to the overall flush plan can be made
|
||||
here; and manipulation of the ``Session`` will not have the
|
||||
desired effect. To manipulate the ``Session`` within an
|
||||
extension, use ``SessionExtension``.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def after_delete(self, mapper, connection, instance):
|
||||
"""Receive an object instance after that instance is deleted.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
class SessionExtension(object):
|
||||
|
||||
"""An extension hook object for Sessions. Subclasses may be
|
||||
installed into a Session (or sessionmaker) using the ``extension``
|
||||
keyword argument. """
|
||||
|
||||
def before_commit(self, session):
|
||||
"""Execute right before commit is called.
|
||||
|
||||
Note that this may not be per-flush if a longer running
|
||||
transaction is ongoing."""
|
||||
|
||||
def after_commit(self, session):
|
||||
"""Execute after a commit has occured.
|
||||
|
||||
Note that this may not be per-flush if a longer running
|
||||
transaction is ongoing."""
|
||||
|
||||
def after_rollback(self, session):
|
||||
"""Execute after a rollback has occured.
|
||||
|
||||
Note that this may not be per-flush if a longer running
|
||||
transaction is ongoing."""
|
||||
|
||||
def before_flush( self, session, flush_context, instances):
|
||||
"""Execute before flush process has started.
|
||||
|
||||
`instances` is an optional list of objects which were passed to
|
||||
the ``flush()`` method. """
|
||||
|
||||
def after_flush(self, session, flush_context):
|
||||
"""Execute after flush has completed, but before commit has been
|
||||
called.
|
||||
|
||||
Note that the session's state is still in pre-flush, i.e. 'new',
|
||||
'dirty', and 'deleted' lists still show pre-flush state as well
|
||||
as the history settings on instance attributes."""
|
||||
|
||||
def after_flush_postexec(self, session, flush_context):
|
||||
"""Execute after flush has completed, and after the post-exec
|
||||
state occurs.
|
||||
|
||||
This will be when the 'new', 'dirty', and 'deleted' lists are in
|
||||
their final state. An actual commit() may or may not have
|
||||
occured, depending on whether or not the flush started its own
|
||||
transaction or participated in a larger transaction. """
|
||||
|
||||
def after_begin( self, session, transaction, connection):
|
||||
"""Execute after a transaction is begun on a connection
|
||||
|
||||
`transaction` is the SessionTransaction. This method is called
|
||||
after an engine level transaction is begun on a connection. """
|
||||
|
||||
def after_attach(self, session, instance):
|
||||
"""Execute after an instance is attached to a session.
|
||||
|
||||
This is called after an add, delete or merge. """
|
||||
|
||||
def after_bulk_update( self, session, query, query_context, result):
|
||||
"""Execute after a bulk update operation to the session.
|
||||
|
||||
This is called after a session.query(...).update()
|
||||
|
||||
`query` is the query object that this update operation was
|
||||
called on. `query_context` was the query context object.
|
||||
`result` is the result object returned from the bulk operation.
|
||||
"""
|
||||
|
||||
def after_bulk_delete( self, session, query, query_context, result):
|
||||
"""Execute after a bulk delete operation to the session.
|
||||
|
||||
This is called after a session.query(...).delete()
|
||||
|
||||
`query` is the query object that this delete operation was
|
||||
called on. `query_context` was the query context object.
|
||||
`result` is the result object returned from the bulk operation.
|
||||
"""
|
||||
|
||||
class MapperProperty(object):
|
||||
"""Manage the relationship of a ``Mapper`` to a single class
|
||||
attribute, as well as that attribute as it appears on individual
|
||||
instances of the class, including attribute instrumentation,
|
||||
attribute access, loading behavior, and dependency calculations.
|
||||
|
||||
The most common occurrences of :class:`.MapperProperty` are the
|
||||
mapped :class:`.Column`, which is represented in a mapping as
|
||||
an instance of :class:`.ColumnProperty`,
|
||||
and a reference to another class produced by :func:`.relationship`,
|
||||
represented in the mapping as an instance of :class:`.RelationshipProperty`.
|
||||
|
||||
"""
|
||||
|
||||
cascade = ()
|
||||
@@ -424,7 +77,7 @@ class MapperProperty(object):
|
||||
|
||||
"""
|
||||
|
||||
def setup(self, context, entity, path, adapter, **kwargs):
|
||||
def setup(self, context, entity, path, reduced_path, adapter, **kwargs):
|
||||
"""Called by Query for the purposes of constructing a SQL statement.
|
||||
|
||||
Each MapperProperty associated with the target mapper processes the
|
||||
@@ -434,12 +87,12 @@ class MapperProperty(object):
|
||||
|
||||
pass
|
||||
|
||||
def create_row_processor(self, selectcontext, path, mapper, row, adapter):
|
||||
def create_row_processor(self, context, path, reduced_path,
|
||||
mapper, row, adapter):
|
||||
"""Return a 3-tuple consisting of three row processing functions.
|
||||
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
return None, None, None
|
||||
|
||||
def cascade_iterator(self, type_, state, visited_instances=None,
|
||||
halt_on=None):
|
||||
@@ -519,9 +172,9 @@ class MapperProperty(object):
|
||||
"""Merge the attribute represented by this ``MapperProperty``
|
||||
from source to destination object"""
|
||||
|
||||
raise NotImplementedError()
|
||||
pass
|
||||
|
||||
def compare(self, operator, value):
|
||||
def compare(self, operator, value, **kw):
|
||||
"""Return a compare operation for the columns represented by
|
||||
this ``MapperProperty`` to the given value, which may be a
|
||||
column value or an instance. 'operator' is an operator from
|
||||
@@ -533,13 +186,13 @@ class MapperProperty(object):
|
||||
|
||||
return operator(self.comparator, value)
|
||||
|
||||
class PropComparator(expression.ColumnOperators):
|
||||
class PropComparator(operators.ColumnOperators):
|
||||
"""Defines comparison operations for MapperProperty objects.
|
||||
|
||||
User-defined subclasses of :class:`.PropComparator` may be created. The
|
||||
built-in Python comparison and math operator methods, such as
|
||||
``__eq__()``, ``__lt__()``, ``__add__()``, can be overridden to provide
|
||||
new operator behaivor. The custom :class:`.PropComparator` is passed to
|
||||
new operator behavior. The custom :class:`.PropComparator` is passed to
|
||||
the mapper property via the ``comparator_factory`` argument. In each case,
|
||||
the appropriate subclass of :class:`.PropComparator` should be used::
|
||||
|
||||
@@ -598,8 +251,7 @@ class PropComparator(expression.ColumnOperators):
|
||||
query.join(Company.employees.of_type(Engineer)).\\
|
||||
filter(Engineer.name=='foo')
|
||||
|
||||
\class_
|
||||
a class or mapper indicating that criterion will be against
|
||||
:param \class_: a class or mapper indicating that criterion will be against
|
||||
this specific subclass.
|
||||
|
||||
|
||||
@@ -611,13 +263,16 @@ class PropComparator(expression.ColumnOperators):
|
||||
"""Return true if this collection contains any member that meets the
|
||||
given criterion.
|
||||
|
||||
criterion
|
||||
an optional ClauseElement formulated against the member class' table
|
||||
or attributes.
|
||||
The usual implementation of ``any()`` is
|
||||
:meth:`.RelationshipProperty.Comparator.any`.
|
||||
|
||||
:param criterion: an optional ClauseElement formulated against the
|
||||
member class' table or attributes.
|
||||
|
||||
:param \**kwargs: key/value pairs corresponding to member class attribute
|
||||
names which will be compared via equality to the corresponding
|
||||
values.
|
||||
|
||||
\**kwargs
|
||||
key/value pairs corresponding to member class attribute names which
|
||||
will be compared via equality to the corresponding values.
|
||||
"""
|
||||
|
||||
return self.operate(PropComparator.any_op, criterion, **kwargs)
|
||||
@@ -626,13 +281,16 @@ class PropComparator(expression.ColumnOperators):
|
||||
"""Return true if this element references a member which meets the
|
||||
given criterion.
|
||||
|
||||
criterion
|
||||
an optional ClauseElement formulated against the member class' table
|
||||
or attributes.
|
||||
The usual implementation of ``has()`` is
|
||||
:meth:`.RelationshipProperty.Comparator.has`.
|
||||
|
||||
:param criterion: an optional ClauseElement formulated against the
|
||||
member class' table or attributes.
|
||||
|
||||
:param \**kwargs: key/value pairs corresponding to member class attribute
|
||||
names which will be compared via equality to the corresponding
|
||||
values.
|
||||
|
||||
\**kwargs
|
||||
key/value pairs corresponding to member class attribute names which
|
||||
will be compared via equality to the corresponding values.
|
||||
"""
|
||||
|
||||
return self.operate(PropComparator.has_op, criterion, **kwargs)
|
||||
@@ -648,38 +306,47 @@ class StrategizedProperty(MapperProperty):
|
||||
|
||||
"""
|
||||
|
||||
def _get_context_strategy(self, context, path):
|
||||
cls = context.attributes.get(('loaderstrategy',
|
||||
_reduce_path(path)), None)
|
||||
strategy_wildcard_key = None
|
||||
|
||||
def _get_context_strategy(self, context, reduced_path):
|
||||
key = ('loaderstrategy', reduced_path)
|
||||
cls = None
|
||||
if key in context.attributes:
|
||||
cls = context.attributes[key]
|
||||
elif self.strategy_wildcard_key:
|
||||
key = ('loaderstrategy', (self.strategy_wildcard_key,))
|
||||
if key in context.attributes:
|
||||
cls = context.attributes[key]
|
||||
|
||||
if cls:
|
||||
try:
|
||||
return self.__all_strategies[cls]
|
||||
return self._strategies[cls]
|
||||
except KeyError:
|
||||
return self.__init_strategy(cls)
|
||||
else:
|
||||
return self.strategy
|
||||
return self.strategy
|
||||
|
||||
def _get_strategy(self, cls):
|
||||
try:
|
||||
return self.__all_strategies[cls]
|
||||
return self._strategies[cls]
|
||||
except KeyError:
|
||||
return self.__init_strategy(cls)
|
||||
|
||||
def __init_strategy(self, cls):
|
||||
self.__all_strategies[cls] = strategy = cls(self)
|
||||
strategy.init()
|
||||
self._strategies[cls] = strategy = cls(self)
|
||||
return strategy
|
||||
|
||||
def setup(self, context, entity, path, adapter, **kwargs):
|
||||
self._get_context_strategy(context, path + (self.key,)).\
|
||||
setup_query(context, entity, path, adapter, **kwargs)
|
||||
def setup(self, context, entity, path, reduced_path, adapter, **kwargs):
|
||||
self._get_context_strategy(context, reduced_path + (self.key,)).\
|
||||
setup_query(context, entity, path,
|
||||
reduced_path, adapter, **kwargs)
|
||||
|
||||
def create_row_processor(self, context, path, mapper, row, adapter):
|
||||
return self._get_context_strategy(context, path + (self.key,)).\
|
||||
create_row_processor(context, path, mapper, row, adapter)
|
||||
def create_row_processor(self, context, path, reduced_path, mapper, row, adapter):
|
||||
return self._get_context_strategy(context, reduced_path + (self.key,)).\
|
||||
create_row_processor(context, path,
|
||||
reduced_path, mapper, row, adapter)
|
||||
|
||||
def do_init(self):
|
||||
self.__all_strategies = {}
|
||||
self._strategies = {}
|
||||
self.strategy = self.__init_strategy(self.strategy_class)
|
||||
|
||||
def post_instrument_class(self, mapper):
|
||||
@@ -706,11 +373,7 @@ def deserialize_path(path):
|
||||
if path is None:
|
||||
return None
|
||||
|
||||
global class_mapper
|
||||
if class_mapper is None:
|
||||
from sqlalchemy.orm import class_mapper
|
||||
|
||||
p = tuple(chain(*[(class_mapper(cls), key) for cls, key in path]))
|
||||
p = tuple(chain(*[(mapperutil.class_mapper(cls), key) for cls, key in path]))
|
||||
if p and p[-1] is None:
|
||||
p = p[0:-1]
|
||||
return p
|
||||
@@ -735,22 +398,10 @@ class MapperOption(object):
|
||||
|
||||
self.process_query(query)
|
||||
|
||||
class ExtensionOption(MapperOption):
|
||||
|
||||
"""a MapperOption that applies a MapperExtension to a query
|
||||
operation."""
|
||||
|
||||
def __init__(self, ext):
|
||||
self.ext = ext
|
||||
|
||||
def process_query(self, query):
|
||||
entity = query._generate_mapper_zero()
|
||||
entity.extension = entity.extension.copy()
|
||||
entity.extension.push(self.ext)
|
||||
|
||||
class PropertyOption(MapperOption):
|
||||
"""A MapperOption that is applied to a property off the mapper or
|
||||
one of its child mappers, identified by a dot-separated key. """
|
||||
one of its child mappers, identified by a dot-separated key
|
||||
or list of class-bound attributes. """
|
||||
|
||||
def __init__(self, key, mapper=None):
|
||||
self.key = key
|
||||
@@ -791,14 +442,12 @@ class PropertyOption(MapperOption):
|
||||
state['key'] = tuple(ret)
|
||||
self.__dict__ = state
|
||||
|
||||
def _find_entity( self, query, mapper, raiseerr):
|
||||
from sqlalchemy.orm.util import _class_to_mapper, \
|
||||
_is_aliased_class
|
||||
if _is_aliased_class(mapper):
|
||||
def _find_entity_prop_comparator(self, query, token, mapper, raiseerr):
|
||||
if mapperutil._is_aliased_class(mapper):
|
||||
searchfor = mapper
|
||||
isa = False
|
||||
else:
|
||||
searchfor = _class_to_mapper(mapper)
|
||||
searchfor = mapperutil._class_to_mapper(mapper)
|
||||
isa = True
|
||||
for ent in query._mapper_entities:
|
||||
if searchfor is ent.path_entity or isa \
|
||||
@@ -806,9 +455,36 @@ class PropertyOption(MapperOption):
|
||||
return ent
|
||||
else:
|
||||
if raiseerr:
|
||||
raise sa_exc.ArgumentError("Can't find entity %s in "
|
||||
"Query. Current list: %r" % (searchfor,
|
||||
[str(m.path_entity) for m in query._entities]))
|
||||
if not list(query._mapper_entities):
|
||||
raise sa_exc.ArgumentError(
|
||||
"Query has only expression-based entities - "
|
||||
"can't find property named '%s'."
|
||||
% (token, )
|
||||
)
|
||||
else:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Can't find property '%s' on any entity "
|
||||
"specified in this Query. Note the full path "
|
||||
"from root (%s) to target entity must be specified."
|
||||
% (token, ",".join(str(x) for
|
||||
x in query._mapper_entities))
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
def _find_entity_basestring(self, query, token, raiseerr):
|
||||
for ent in query._mapper_entities:
|
||||
# return only the first _MapperEntity when searching
|
||||
# based on string prop name. Ideally object
|
||||
# attributes are used to specify more exactly.
|
||||
return ent
|
||||
else:
|
||||
if raiseerr:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Query has only expression-based entities - "
|
||||
"can't find property named '%s'."
|
||||
% (token, )
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -818,114 +494,112 @@ class PropertyOption(MapperOption):
|
||||
l = []
|
||||
mappers = []
|
||||
|
||||
# _current_path implies we're in a secondary load with an
|
||||
# existing path
|
||||
|
||||
# _current_path implies we're in a
|
||||
# secondary load with an existing path
|
||||
current_path = list(query._current_path)
|
||||
tokens = []
|
||||
for key in util.to_list(self.key):
|
||||
if isinstance(key, basestring):
|
||||
tokens += key.split('.')
|
||||
else:
|
||||
tokens += [key]
|
||||
for token in tokens:
|
||||
|
||||
tokens = deque(self.key)
|
||||
while tokens:
|
||||
token = tokens.popleft()
|
||||
if isinstance(token, basestring):
|
||||
# wildcard token
|
||||
if token.endswith(':*'):
|
||||
return [(token,)], []
|
||||
sub_tokens = token.split(".", 1)
|
||||
token = sub_tokens[0]
|
||||
tokens.extendleft(sub_tokens[1:])
|
||||
|
||||
# exhaust current_path before
|
||||
# matching tokens to entities
|
||||
if current_path:
|
||||
if current_path[1] == token:
|
||||
current_path = current_path[2:]
|
||||
continue
|
||||
else:
|
||||
return [], []
|
||||
|
||||
if not entity:
|
||||
if current_path:
|
||||
if current_path[1] == token:
|
||||
current_path = current_path[2:]
|
||||
continue
|
||||
entity = query._entity_zero()
|
||||
entity = self._find_entity_basestring(
|
||||
query,
|
||||
token,
|
||||
raiseerr)
|
||||
if entity is None:
|
||||
return [], []
|
||||
path_element = entity.path_entity
|
||||
mapper = entity.mapper
|
||||
mappers.append(mapper)
|
||||
prop = mapper.get_property(token,
|
||||
resolve_synonyms=True, raiseerr=raiseerr)
|
||||
key = token
|
||||
if hasattr(mapper.class_, token):
|
||||
prop = getattr(mapper.class_, token).property
|
||||
else:
|
||||
if raiseerr:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Can't find property named '%s' on the "
|
||||
"mapped entity %s in this Query. " % (
|
||||
token, mapper)
|
||||
)
|
||||
else:
|
||||
return [], []
|
||||
elif isinstance(token, PropComparator):
|
||||
prop = token.property
|
||||
|
||||
# exhaust current_path before
|
||||
# matching tokens to entities
|
||||
if current_path:
|
||||
if current_path[0:2] == \
|
||||
[token.parententity, prop.key]:
|
||||
current_path = current_path[2:]
|
||||
continue
|
||||
else:
|
||||
return [], []
|
||||
|
||||
if not entity:
|
||||
if current_path:
|
||||
if current_path[0:2] == [token.parententity,
|
||||
prop.key]:
|
||||
current_path = current_path[2:]
|
||||
continue
|
||||
entity = self._find_entity(query,
|
||||
token.parententity, raiseerr)
|
||||
entity = self._find_entity_prop_comparator(
|
||||
query,
|
||||
prop.key,
|
||||
token.parententity,
|
||||
raiseerr)
|
||||
if not entity:
|
||||
return [], []
|
||||
path_element = entity.path_entity
|
||||
mapper = entity.mapper
|
||||
mappers.append(prop.parent)
|
||||
key = prop.key
|
||||
else:
|
||||
raise sa_exc.ArgumentError('mapper option expects '
|
||||
'string key or list of attributes')
|
||||
if prop is None:
|
||||
return [], []
|
||||
raise sa_exc.ArgumentError(
|
||||
"mapper option expects "
|
||||
"string key or list of attributes")
|
||||
assert prop is not None
|
||||
if raiseerr and not prop.parent.common_parent(mapper):
|
||||
raise sa_exc.ArgumentError("Attribute '%s' does not "
|
||||
"link from element '%s'" % (token, path_element))
|
||||
|
||||
path = build_path(path_element, prop.key, path)
|
||||
|
||||
l.append(path)
|
||||
if getattr(token, '_of_type', None):
|
||||
path_element = mapper = token._of_type
|
||||
else:
|
||||
path_element = mapper = getattr(prop, 'mapper', None)
|
||||
if path_element:
|
||||
path_element = path_element
|
||||
if mapper is None and tokens:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Attribute '%s' of entity '%s' does not "
|
||||
"refer to a mapped entity" %
|
||||
(token, entity)
|
||||
)
|
||||
|
||||
if current_path:
|
||||
# ran out of tokens before
|
||||
# current_path was exhausted.
|
||||
assert not tokens
|
||||
return [], []
|
||||
|
||||
return l, mappers
|
||||
|
||||
class AttributeExtension(object):
|
||||
"""An event handler for individual attribute change events.
|
||||
|
||||
AttributeExtension is assembled within the descriptors associated
|
||||
with a mapped class.
|
||||
|
||||
"""
|
||||
|
||||
active_history = True
|
||||
"""indicates that the set() method would like to receive the 'old' value,
|
||||
even if it means firing lazy callables.
|
||||
|
||||
Note that ``active_history`` can also be set directly via
|
||||
:func:`.column_property` and :func:`.relationship`.
|
||||
|
||||
"""
|
||||
|
||||
def append(self, state, value, initiator):
|
||||
"""Receive a collection append event.
|
||||
|
||||
The returned value will be used as the actual value to be
|
||||
appended.
|
||||
|
||||
"""
|
||||
return value
|
||||
|
||||
def remove(self, state, value, initiator):
|
||||
"""Receive a remove event.
|
||||
|
||||
No return value is defined.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def set(self, state, value, oldvalue, initiator):
|
||||
"""Receive a set event.
|
||||
|
||||
The returned value will be used as the actual value to be
|
||||
set.
|
||||
|
||||
"""
|
||||
return value
|
||||
|
||||
|
||||
class StrategizedOption(PropertyOption):
|
||||
"""A MapperOption that affects which LoaderStrategy will be used
|
||||
for an operation by a StrategizedProperty.
|
||||
"""
|
||||
|
||||
is_chained = False
|
||||
chained = False
|
||||
|
||||
def process_query_property(self, query, paths, mappers):
|
||||
|
||||
@@ -935,7 +609,7 @@ class StrategizedOption(PropertyOption):
|
||||
# "(Person, 'machines')" in the path due to the mechanics of how
|
||||
# the eager strategy builds up the path
|
||||
|
||||
if self.is_chained:
|
||||
if self.chained:
|
||||
for path in paths:
|
||||
query._attributes[('loaderstrategy',
|
||||
_reduce_path(path))] = \
|
||||
@@ -953,13 +627,13 @@ def _reduce_path(path):
|
||||
|
||||
This is used to allow more open ended selection of loader strategies, i.e.
|
||||
Mapper -> prop1 -> Subclass -> prop2, where Subclass is a sub-mapper
|
||||
of the mapper referened by Mapper.prop1.
|
||||
of the mapper referenced by Mapper.prop1.
|
||||
|
||||
"""
|
||||
return tuple([i % 2 != 0 and
|
||||
path[i] or
|
||||
getattr(path[i], 'base_mapper', path[i])
|
||||
for i in xrange(len(path))])
|
||||
element or
|
||||
getattr(element, 'base_mapper', element)
|
||||
for i, element in enumerate(path)])
|
||||
|
||||
class LoaderStrategy(object):
|
||||
"""Describe the loading behavior of a StrategizedProperty object.
|
||||
@@ -975,22 +649,25 @@ class LoaderStrategy(object):
|
||||
|
||||
* it processes the ``QueryContext`` at statement construction time,
|
||||
where it can modify the SQL statement that is being produced.
|
||||
simple column attributes may add their represented column to the
|
||||
Simple column attributes may add their represented column to the
|
||||
list of selected columns, *eager loading* properties may add
|
||||
``LEFT OUTER JOIN`` clauses to the statement.
|
||||
|
||||
* it processes the ``SelectionContext`` at row-processing time. This
|
||||
includes straight population of attributes corresponding to rows,
|
||||
setting instance-level lazyloader callables on newly
|
||||
constructed instances, and appending child items to scalar/collection
|
||||
attributes in response to eagerly-loaded relations.
|
||||
"""
|
||||
* It produces "row processor" functions at result fetching time.
|
||||
These "row processor" functions populate a particular attribute
|
||||
on a particular mapped instance.
|
||||
|
||||
"""
|
||||
def __init__(self, parent):
|
||||
self.parent_property = parent
|
||||
self.is_class_level = False
|
||||
self.parent = self.parent_property.parent
|
||||
self.key = self.parent_property.key
|
||||
# TODO: there's no particular reason we need
|
||||
# the separate .init() method at this point.
|
||||
# It's possible someone has written their
|
||||
# own LS object.
|
||||
self.init()
|
||||
|
||||
def init(self):
|
||||
raise NotImplementedError("LoaderStrategy")
|
||||
@@ -998,10 +675,10 @@ class LoaderStrategy(object):
|
||||
def init_class_attribute(self, mapper):
|
||||
pass
|
||||
|
||||
def setup_query(self, context, entity, path, adapter, **kwargs):
|
||||
def setup_query(self, context, entity, path, reduced_path, adapter, **kwargs):
|
||||
pass
|
||||
|
||||
def create_row_processor(self, selectcontext, path, mapper,
|
||||
def create_row_processor(self, context, path, reduced_path, mapper,
|
||||
row, adapter):
|
||||
"""Return row processing functions which fulfill the contract
|
||||
specified by MapperProperty.create_row_processor.
|
||||
@@ -1009,7 +686,7 @@ class LoaderStrategy(object):
|
||||
StrategizedProperty delegates its create_row_processor method
|
||||
directly to this method. """
|
||||
|
||||
raise NotImplementedError()
|
||||
return None, None, None
|
||||
|
||||
def __str__(self):
|
||||
return str(self.parent_property)
|
||||
@@ -1028,6 +705,18 @@ class LoaderStrategy(object):
|
||||
class InstrumentationManager(object):
|
||||
"""User-defined class instrumentation extension.
|
||||
|
||||
:class:`.InstrumentationManager` can be subclassed in order
|
||||
to change
|
||||
how class instrumentation proceeds. This class exists for
|
||||
the purposes of integration with other object management
|
||||
frameworks which would like to entirely modify the
|
||||
instrumentation methodology of the ORM, and is not intended
|
||||
for regular usage. For interception of class instrumentation
|
||||
events, see :class:`.InstrumentationEvents`.
|
||||
|
||||
For an example of :class:`.InstrumentationManager`, see the
|
||||
example :ref:`examples_instrumentation`.
|
||||
|
||||
The API for this class should be considered as semi-stable,
|
||||
and may change slightly with new releases.
|
||||
|
||||
@@ -1085,7 +774,7 @@ class InstrumentationManager(object):
|
||||
setattr(instance, '_default_state', state)
|
||||
|
||||
def remove_state(self, class_, instance):
|
||||
delattr(instance, '_default_state', state)
|
||||
delattr(instance, '_default_state')
|
||||
|
||||
def state_getter(self, class_):
|
||||
return lambda instance: getattr(instance, '_default_state')
|
||||
|
||||
+1054
-585
File diff suppressed because it is too large
Load Diff
+659
-592
File diff suppressed because it is too large
Load Diff
+837
-426
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,12 @@
|
||||
# orm/scoping.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 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
|
||||
|
||||
import sqlalchemy.exceptions as sa_exc
|
||||
from sqlalchemy.util import ScopedRegistry, ThreadLocalRegistry, \
|
||||
to_list, get_cls_kwargs, deprecated,\
|
||||
warn
|
||||
from sqlalchemy.orm import (
|
||||
EXT_CONTINUE, MapperExtension, class_mapper, object_session
|
||||
)
|
||||
from sqlalchemy import exc as sa_exc
|
||||
from sqlalchemy.util import ScopedRegistry, ThreadLocalRegistry, warn
|
||||
from sqlalchemy.orm import class_mapper
|
||||
from sqlalchemy.orm import exc as orm_exc
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
@@ -20,16 +16,16 @@ __all__ = ['ScopedSession']
|
||||
|
||||
class ScopedSession(object):
|
||||
"""Provides thread-local management of Sessions.
|
||||
|
||||
Usage::
|
||||
|
||||
|
||||
Typical invocation is via the :func:`.scoped_session`
|
||||
function::
|
||||
|
||||
Session = scoped_session(sessionmaker())
|
||||
|
||||
... use Session normally.
|
||||
|
||||
The internal registry is accessible as well,
|
||||
The internal registry is accessible,
|
||||
and by default is an instance of :class:`.ThreadLocalRegistry`.
|
||||
|
||||
See also: :ref:`unitofwork_contextual`.
|
||||
|
||||
"""
|
||||
|
||||
@@ -39,7 +35,6 @@ class ScopedSession(object):
|
||||
self.registry = ScopedRegistry(session_factory, scopefunc)
|
||||
else:
|
||||
self.registry = ThreadLocalRegistry(session_factory)
|
||||
self.extension = _ScopedExt(self)
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
if kwargs:
|
||||
@@ -64,27 +59,6 @@ class ScopedSession(object):
|
||||
self.registry().close()
|
||||
self.registry.clear()
|
||||
|
||||
@deprecated("0.5", ":meth:`.ScopedSession.mapper` is deprecated. "
|
||||
"Please see http://www.sqlalchemy.org/trac/wiki/UsageRecipes/SessionAwareMapper "
|
||||
"for information on how to replicate its behavior.")
|
||||
def mapper(self, *args, **kwargs):
|
||||
"""return a :func:`.mapper` function which associates this ScopedSession with the Mapper.
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm import mapper
|
||||
|
||||
extension_args = dict((arg, kwargs.pop(arg))
|
||||
for arg in get_cls_kwargs(_ScopedExt)
|
||||
if arg in kwargs)
|
||||
|
||||
kwargs['extension'] = extension = to_list(kwargs.get('extension', []))
|
||||
if extension_args:
|
||||
extension.append(self.extension.configure(**extension_args))
|
||||
else:
|
||||
extension.append(self.extension)
|
||||
return mapper(*args, **kwargs)
|
||||
|
||||
def configure(self, **kwargs):
|
||||
"""reconfigure the sessionmaker used by this ScopedSession."""
|
||||
|
||||
@@ -157,59 +131,3 @@ def clslevel(name):
|
||||
for prop in ('close_all', 'object_session', 'identity_key'):
|
||||
setattr(ScopedSession, prop, clslevel(prop))
|
||||
|
||||
class _ScopedExt(MapperExtension):
|
||||
def __init__(self, context, validate=False, save_on_init=True):
|
||||
self.context = context
|
||||
self.validate = validate
|
||||
self.save_on_init = save_on_init
|
||||
self.set_kwargs_on_init = True
|
||||
|
||||
def validating(self):
|
||||
return _ScopedExt(self.context, validate=True)
|
||||
|
||||
def configure(self, **kwargs):
|
||||
return _ScopedExt(self.context, **kwargs)
|
||||
|
||||
def instrument_class(self, mapper, class_):
|
||||
class query(object):
|
||||
def __getattr__(s, key):
|
||||
return getattr(self.context.registry().query(class_), key)
|
||||
def __call__(s):
|
||||
return self.context.registry().query(class_)
|
||||
def __get__(self, instance, cls):
|
||||
return self
|
||||
|
||||
if not 'query' in class_.__dict__:
|
||||
class_.query = query()
|
||||
|
||||
if self.set_kwargs_on_init and class_.__init__ is object.__init__:
|
||||
class_.__init__ = self._default__init__(mapper)
|
||||
|
||||
def _default__init__(ext, mapper):
|
||||
def __init__(self, **kwargs):
|
||||
for key, value in kwargs.iteritems():
|
||||
if ext.validate:
|
||||
if not mapper.get_property(key, resolve_synonyms=False,
|
||||
raiseerr=False):
|
||||
raise sa_exc.ArgumentError(
|
||||
"Invalid __init__ argument: '%s'" % key)
|
||||
setattr(self, key, value)
|
||||
return __init__
|
||||
|
||||
def init_instance(self, mapper, class_, oldinit, instance, args, kwargs):
|
||||
if self.save_on_init:
|
||||
session = kwargs.pop('_sa_session', None)
|
||||
if session is None:
|
||||
session = self.context.registry()
|
||||
session._save_without_cascade(instance)
|
||||
return EXT_CONTINUE
|
||||
|
||||
def init_failed(self, mapper, class_, oldinit, instance, args, kwargs):
|
||||
sess = object_session(instance)
|
||||
if sess:
|
||||
sess.expunge(instance)
|
||||
return EXT_CONTINUE
|
||||
|
||||
def dispose_class(self, mapper, class_):
|
||||
if hasattr(class_, 'query'):
|
||||
delattr(class_, 'query')
|
||||
|
||||
+523
-343
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
# orm/shard.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 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
|
||||
|
||||
+159
-141
@@ -1,19 +1,28 @@
|
||||
# orm/state.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 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
|
||||
|
||||
"""Defines instrumentation of instances.
|
||||
|
||||
This module is usually not directly visible to user applications, but
|
||||
defines a large part of the ORM's interactivity.
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy.util import EMPTY_SET
|
||||
import weakref
|
||||
from sqlalchemy import util
|
||||
from sqlalchemy.orm.attributes import PASSIVE_NO_RESULT, PASSIVE_OFF, \
|
||||
NEVER_SET, NO_VALUE, manager_of_class, \
|
||||
ATTR_WAS_SET
|
||||
from sqlalchemy.orm import attributes, exc as orm_exc, interfaces
|
||||
|
||||
from sqlalchemy.orm import exc as orm_exc, attributes, interfaces,\
|
||||
util as orm_util
|
||||
from sqlalchemy.orm.attributes import PASSIVE_OFF, PASSIVE_NO_RESULT, \
|
||||
PASSIVE_NO_FETCH, NEVER_SET, ATTR_WAS_SET, NO_VALUE
|
||||
|
||||
mapperlib = util.importlater("sqlalchemy.orm", "mapperlib")
|
||||
|
||||
import sys
|
||||
attributes.state = sys.modules['sqlalchemy.orm.state']
|
||||
|
||||
class InstanceState(object):
|
||||
"""tracks state information at the instance level."""
|
||||
@@ -34,10 +43,8 @@ class InstanceState(object):
|
||||
self.class_ = obj.__class__
|
||||
self.manager = manager
|
||||
self.obj = weakref.ref(obj, self._cleanup)
|
||||
|
||||
@util.memoized_property
|
||||
def committed_state(self):
|
||||
return {}
|
||||
self.callables = {}
|
||||
self.committed_state = {}
|
||||
|
||||
@util.memoized_property
|
||||
def parents(self):
|
||||
@@ -47,20 +54,12 @@ class InstanceState(object):
|
||||
def pending(self):
|
||||
return {}
|
||||
|
||||
@util.memoized_property
|
||||
def callables(self):
|
||||
return {}
|
||||
|
||||
@property
|
||||
def has_identity(self):
|
||||
return bool(self.key)
|
||||
|
||||
def detach(self):
|
||||
if self.session_id:
|
||||
try:
|
||||
del self.session_id
|
||||
except AttributeError:
|
||||
pass
|
||||
self.session_id = None
|
||||
|
||||
def dispose(self):
|
||||
self.detach()
|
||||
@@ -69,13 +68,11 @@ class InstanceState(object):
|
||||
def _cleanup(self, ref):
|
||||
instance_dict = self._instance_dict()
|
||||
if instance_dict:
|
||||
try:
|
||||
instance_dict.remove(self)
|
||||
except AssertionError:
|
||||
pass
|
||||
# remove possible cycles
|
||||
self.__dict__.pop('callables', None)
|
||||
self.dispose()
|
||||
instance_dict.discard(self)
|
||||
|
||||
self.callables = {}
|
||||
self.session_id = None
|
||||
del self.obj
|
||||
|
||||
def obj(self):
|
||||
return None
|
||||
@@ -88,90 +85,88 @@ class InstanceState(object):
|
||||
else:
|
||||
return {}
|
||||
|
||||
@property
|
||||
def sort_key(self):
|
||||
return self.key and self.key[1] or (self.insert_order, )
|
||||
|
||||
def initialize_instance(*mixed, **kwargs):
|
||||
self, instance, args = mixed[0], mixed[1], mixed[2:]
|
||||
manager = self.manager
|
||||
|
||||
for fn in manager.events.on_init:
|
||||
fn(self, instance, args, kwargs)
|
||||
manager.dispatch.init(self, args, kwargs)
|
||||
|
||||
# LESSTHANIDEAL:
|
||||
# adjust for the case where the InstanceState was created before
|
||||
# mapper compilation, and this actually needs to be a MutableAttrInstanceState
|
||||
if manager.mutable_attributes and self.__class__ is not MutableAttrInstanceState:
|
||||
self.__class__ = MutableAttrInstanceState
|
||||
self.obj = weakref.ref(self.obj(), self._cleanup)
|
||||
self.mutable_dict = {}
|
||||
#if manager.mutable_attributes:
|
||||
# assert self.__class__ is MutableAttrInstanceState
|
||||
|
||||
try:
|
||||
return manager.events.original_init(*mixed[1:], **kwargs)
|
||||
return manager.original_init(*mixed[1:], **kwargs)
|
||||
except:
|
||||
for fn in manager.events.on_init_failure:
|
||||
fn(self, instance, args, kwargs)
|
||||
manager.dispatch.init_failure(self, args, kwargs)
|
||||
raise
|
||||
|
||||
def get_history(self, key, **kwargs):
|
||||
return self.manager.get_impl(key).get_history(self, self.dict, **kwargs)
|
||||
def get_history(self, key, passive):
|
||||
return self.manager[key].impl.get_history(self, self.dict, passive)
|
||||
|
||||
def get_impl(self, key):
|
||||
return self.manager.get_impl(key)
|
||||
return self.manager[key].impl
|
||||
|
||||
def get_pending(self, key):
|
||||
if key not in self.pending:
|
||||
self.pending[key] = PendingCollection()
|
||||
return self.pending[key]
|
||||
|
||||
def value_as_iterable(self, key, passive=PASSIVE_OFF):
|
||||
"""return an InstanceState attribute as a list,
|
||||
regardless of it being a scalar or collection-based
|
||||
attribute.
|
||||
def value_as_iterable(self, dict_, key, passive=PASSIVE_OFF):
|
||||
"""Return a list of tuples (state, obj) for the given
|
||||
key.
|
||||
|
||||
returns None if passive is not PASSIVE_OFF and the getter returns
|
||||
PASSIVE_NO_RESULT.
|
||||
returns an empty list if the value is None/empty/PASSIVE_NO_RESULT
|
||||
"""
|
||||
|
||||
impl = self.get_impl(key)
|
||||
dict_ = self.dict
|
||||
impl = self.manager[key].impl
|
||||
x = impl.get(self, dict_, passive=passive)
|
||||
if x is PASSIVE_NO_RESULT:
|
||||
return None
|
||||
if x is PASSIVE_NO_RESULT or x is None:
|
||||
return []
|
||||
elif hasattr(impl, 'get_collection'):
|
||||
return impl.get_collection(self, dict_, x, passive=passive)
|
||||
return [
|
||||
(attributes.instance_state(o), o) for o in
|
||||
impl.get_collection(self, dict_, x, passive=passive)
|
||||
]
|
||||
else:
|
||||
return [x]
|
||||
|
||||
def _run_on_load(self, instance):
|
||||
self.manager.events.run('on_load', instance)
|
||||
return [(attributes.instance_state(x), x)]
|
||||
|
||||
def __getstate__(self):
|
||||
d = {'instance':self.obj()}
|
||||
|
||||
d.update(
|
||||
(k, self.__dict__[k]) for k in (
|
||||
'committed_state', 'pending', 'parents', 'modified', 'expired',
|
||||
'callables', 'key', 'load_options', 'mutable_dict'
|
||||
'committed_state', 'pending', 'modified', 'expired',
|
||||
'callables', 'key', 'parents', 'load_options', 'mutable_dict',
|
||||
'class_',
|
||||
) if k in self.__dict__
|
||||
)
|
||||
if self.load_path:
|
||||
d['load_path'] = interfaces.serialize_path(self.load_path)
|
||||
|
||||
self.manager.dispatch.pickle(self, d)
|
||||
|
||||
return d
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.obj = weakref.ref(state['instance'], self._cleanup)
|
||||
self.class_ = state['instance'].__class__
|
||||
self.manager = manager = manager_of_class(self.class_)
|
||||
from sqlalchemy.orm import instrumentation
|
||||
inst = state['instance']
|
||||
if inst is not None:
|
||||
self.obj = weakref.ref(inst, self._cleanup)
|
||||
self.class_ = inst.__class__
|
||||
else:
|
||||
# None being possible here generally new as of 0.7.4
|
||||
# due to storage of state in "parents". "class_"
|
||||
# also new.
|
||||
self.obj = None
|
||||
self.class_ = state['class_']
|
||||
self.manager = manager = instrumentation.manager_of_class(self.class_)
|
||||
if manager is None:
|
||||
raise orm_exc.UnmappedInstanceError(
|
||||
state['instance'],
|
||||
inst,
|
||||
"Cannot deserialize object of type %r - no mapper() has"
|
||||
" been configured for this class within the current Python process!" %
|
||||
self.class_)
|
||||
elif manager.is_mapped and not manager.mapper.compiled:
|
||||
manager.mapper.compile()
|
||||
elif manager.is_mapped and not manager.mapper.configured:
|
||||
mapperlib.configure_mappers()
|
||||
|
||||
self.committed_state = state.get('committed_state', {})
|
||||
self.pending = state.get('pending', {})
|
||||
@@ -181,7 +176,7 @@ class InstanceState(object):
|
||||
self.callables = state.get('callables', {})
|
||||
|
||||
if self.modified:
|
||||
self._strong_obj = state['instance']
|
||||
self._strong_obj = inst
|
||||
|
||||
self.__dict__.update([
|
||||
(k, state[k]) for k in (
|
||||
@@ -192,6 +187,12 @@ class InstanceState(object):
|
||||
if 'load_path' in state:
|
||||
self.load_path = interfaces.deserialize_path(state['load_path'])
|
||||
|
||||
# setup _sa_instance_state ahead of time so that
|
||||
# unpickle events can access the object normally.
|
||||
# see [ticket:2362]
|
||||
manager.setup_instance(inst, self)
|
||||
manager.dispatch.unpickle(self, state)
|
||||
|
||||
def initialize(self, key):
|
||||
"""Set this attribute to an empty value or collection,
|
||||
based on the AttributeImpl in use."""
|
||||
@@ -224,62 +225,59 @@ class InstanceState(object):
|
||||
dict_.pop(key, None)
|
||||
self.callables[key] = callable_
|
||||
|
||||
def expire_attributes(self, dict_, attribute_names, instance_dict=None):
|
||||
"""Expire all or a group of attributes.
|
||||
def expire(self, dict_, modified_set):
|
||||
self.expired = True
|
||||
if self.modified:
|
||||
modified_set.discard(self)
|
||||
|
||||
If all attributes are expired, the "expired" flag is set to True.
|
||||
self.modified = False
|
||||
|
||||
"""
|
||||
# we would like to assert that 'self.key is not None' here,
|
||||
# but there are many cases where the mapper will expire
|
||||
# a newly persisted instance within the flush, before the
|
||||
# key is assigned, and even cases where the attribute refresh
|
||||
# occurs fully, within the flush(), before this key is assigned.
|
||||
# the key is assigned late within the flush() to assist in
|
||||
# "key switch" bookkeeping scenarios.
|
||||
self.committed_state.clear()
|
||||
|
||||
if attribute_names is None:
|
||||
attribute_names = self.manager.keys()
|
||||
self.expired = True
|
||||
if self.modified:
|
||||
if not instance_dict:
|
||||
instance_dict = self._instance_dict()
|
||||
if instance_dict:
|
||||
instance_dict._modified.discard(self)
|
||||
else:
|
||||
instance_dict._modified.discard(self)
|
||||
self.__dict__.pop('pending', None)
|
||||
self.__dict__.pop('mutable_dict', None)
|
||||
|
||||
self.modified = False
|
||||
filter_deferred = True
|
||||
else:
|
||||
filter_deferred = False
|
||||
# clear out 'parents' collection. not
|
||||
# entirely clear how we can best determine
|
||||
# which to remove, or not.
|
||||
self.__dict__.pop('parents', None)
|
||||
|
||||
to_clear = (
|
||||
self.__dict__.get('pending', None),
|
||||
self.__dict__.get('committed_state', None),
|
||||
self.mutable_dict
|
||||
)
|
||||
|
||||
for key in attribute_names:
|
||||
for key in self.manager:
|
||||
impl = self.manager[key].impl
|
||||
if impl.accepts_scalar_loader and \
|
||||
(not filter_deferred or impl.expire_missing or key in dict_):
|
||||
(impl.expire_missing or key in dict_):
|
||||
self.callables[key] = self
|
||||
dict_.pop(key, None)
|
||||
|
||||
for d in to_clear:
|
||||
if d is not None:
|
||||
d.pop(key, None)
|
||||
self.manager.dispatch.expire(self, None)
|
||||
|
||||
def __call__(self, **kw):
|
||||
def expire_attributes(self, dict_, attribute_names):
|
||||
pending = self.__dict__.get('pending', None)
|
||||
mutable_dict = self.mutable_dict
|
||||
|
||||
for key in attribute_names:
|
||||
impl = self.manager[key].impl
|
||||
if impl.accepts_scalar_loader:
|
||||
self.callables[key] = self
|
||||
dict_.pop(key, None)
|
||||
|
||||
self.committed_state.pop(key, None)
|
||||
if mutable_dict:
|
||||
mutable_dict.pop(key, None)
|
||||
if pending:
|
||||
pending.pop(key, None)
|
||||
|
||||
self.manager.dispatch.expire(self, attribute_names)
|
||||
|
||||
def __call__(self, passive):
|
||||
"""__call__ allows the InstanceState to act as a deferred
|
||||
callable for loading expired attributes, which is also
|
||||
serializable (picklable).
|
||||
|
||||
"""
|
||||
|
||||
if kw.get('passive') is attributes.PASSIVE_NO_FETCH:
|
||||
return attributes.PASSIVE_NO_RESULT
|
||||
if passive is PASSIVE_NO_FETCH:
|
||||
return PASSIVE_NO_RESULT
|
||||
|
||||
toload = self.expired_attributes.\
|
||||
intersection(self.unmodified)
|
||||
@@ -301,6 +299,13 @@ class InstanceState(object):
|
||||
|
||||
return set(self.manager).difference(self.committed_state)
|
||||
|
||||
def unmodified_intersection(self, keys):
|
||||
"""Return self.unmodified.intersection(keys)."""
|
||||
|
||||
return set(keys).intersection(self.manager).\
|
||||
difference(self.committed_state)
|
||||
|
||||
|
||||
@property
|
||||
def unloaded(self):
|
||||
"""Return the set of keys which do not have a loaded value.
|
||||
@@ -331,21 +336,18 @@ class InstanceState(object):
|
||||
def _is_really_none(self):
|
||||
return self.obj()
|
||||
|
||||
def modified_event(self, dict_, attr, should_copy, previous, passive=PASSIVE_OFF):
|
||||
def modified_event(self, dict_, attr, previous, collection=False):
|
||||
if attr.key not in self.committed_state:
|
||||
if previous is NEVER_SET:
|
||||
if passive:
|
||||
if collection:
|
||||
if previous is NEVER_SET:
|
||||
if attr.key in dict_:
|
||||
previous = dict_[attr.key]
|
||||
else:
|
||||
previous = attr.get(self, dict_)
|
||||
|
||||
if should_copy and previous not in (None, NO_VALUE, NEVER_SET):
|
||||
previous = attr.copy(previous)
|
||||
if previous not in (None, NO_VALUE, NEVER_SET):
|
||||
previous = attr.copy(previous)
|
||||
|
||||
self.committed_state[attr.key] = previous
|
||||
|
||||
|
||||
# the "or not self.modified" is defensive at
|
||||
# this point. The assertion below is expected
|
||||
# to be True:
|
||||
@@ -357,7 +359,15 @@ class InstanceState(object):
|
||||
instance_dict._modified.add(self)
|
||||
|
||||
self._strong_obj = self.obj()
|
||||
|
||||
if self._strong_obj is None:
|
||||
raise orm_exc.ObjectDereferencedError(
|
||||
"Can't emit change event for attribute '%s' - "
|
||||
"parent object of type %s has been garbage "
|
||||
"collected."
|
||||
% (
|
||||
self.manager[attr.key],
|
||||
orm_util.state_class_str(self)
|
||||
))
|
||||
self.modified = True
|
||||
|
||||
def commit(self, dict_, keys):
|
||||
@@ -371,10 +381,14 @@ class InstanceState(object):
|
||||
|
||||
"""
|
||||
class_manager = self.manager
|
||||
for key in keys:
|
||||
if key in dict_ and key in class_manager.mutable_attributes:
|
||||
self.committed_state[key] = self.manager[key].impl.copy(dict_[key])
|
||||
else:
|
||||
if class_manager.mutable_attributes:
|
||||
for key in keys:
|
||||
if key in dict_ and key in class_manager.mutable_attributes:
|
||||
self.committed_state[key] = self.manager[key].impl.copy(dict_[key])
|
||||
else:
|
||||
self.committed_state.pop(key, None)
|
||||
else:
|
||||
for key in keys:
|
||||
self.committed_state.pop(key, None)
|
||||
|
||||
self.expired = False
|
||||
@@ -400,14 +414,13 @@ class InstanceState(object):
|
||||
|
||||
"""
|
||||
|
||||
self.__dict__.pop('committed_state', None)
|
||||
self.committed_state.clear()
|
||||
self.__dict__.pop('pending', None)
|
||||
|
||||
if 'callables' in self.__dict__:
|
||||
callables = self.callables
|
||||
for key in list(callables):
|
||||
if key in dict_ and callables[key] is self:
|
||||
del callables[key]
|
||||
callables = self.callables
|
||||
for key in list(callables):
|
||||
if key in dict_ and callables[key] is self:
|
||||
del callables[key]
|
||||
|
||||
for key in self.manager.mutable_attributes:
|
||||
if key in dict_:
|
||||
@@ -461,6 +474,18 @@ class MutableAttrInstanceState(InstanceState):
|
||||
(key in self.manager.mutable_attributes and
|
||||
not self.manager[key].impl.check_mutable_modified(self, dict_)))])
|
||||
|
||||
def unmodified_intersection(self, keys):
|
||||
"""Return self.unmodified.intersection(keys)."""
|
||||
|
||||
dict_ = self.dict
|
||||
|
||||
return set([
|
||||
key for key in keys
|
||||
if (key not in self.committed_state or
|
||||
(key in self.manager.mutable_attributes and
|
||||
not self.manager[key].impl.check_mutable_modified(self, dict_)))])
|
||||
|
||||
|
||||
def _is_really_none(self):
|
||||
"""do a check modified/resurrect.
|
||||
|
||||
@@ -498,10 +523,7 @@ class MutableAttrInstanceState(InstanceState):
|
||||
else:
|
||||
instance_dict = self._instance_dict()
|
||||
if instance_dict:
|
||||
try:
|
||||
instance_dict.remove(self)
|
||||
except AssertionError:
|
||||
pass
|
||||
instance_dict.discard(self)
|
||||
self.dispose()
|
||||
|
||||
def __resurrect(self):
|
||||
@@ -509,19 +531,13 @@ class MutableAttrInstanceState(InstanceState):
|
||||
|
||||
# store strong ref'ed version of the object; will revert
|
||||
# to weakref when changes are persisted
|
||||
|
||||
obj = self.manager.new_instance(state=self)
|
||||
self.obj = weakref.ref(obj, self._cleanup)
|
||||
self._strong_obj = obj
|
||||
obj.__dict__.update(self.mutable_dict)
|
||||
|
||||
# re-establishes identity attributes from the key
|
||||
self.manager.events.run('on_resurrect', self, obj)
|
||||
|
||||
# TODO: don't really think we should run this here.
|
||||
# resurrect is only meant to preserve the minimal state needed to
|
||||
# do an UPDATE, not to produce a fully usable object
|
||||
self._run_on_load(obj)
|
||||
self.manager.dispatch.resurrect(self)
|
||||
|
||||
return obj
|
||||
|
||||
@@ -540,10 +556,12 @@ class PendingCollection(object):
|
||||
def append(self, value):
|
||||
if value in self.deleted_items:
|
||||
self.deleted_items.remove(value)
|
||||
self.added_items.add(value)
|
||||
else:
|
||||
self.added_items.add(value)
|
||||
|
||||
def remove(self, value):
|
||||
if value in self.added_items:
|
||||
self.added_items.remove(value)
|
||||
self.deleted_items.add(value)
|
||||
else:
|
||||
self.deleted_items.add(value)
|
||||
|
||||
|
||||
+565
-481
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
# orm/sync.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 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
|
||||
@@ -8,28 +8,37 @@
|
||||
between instances based on join conditions.
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm import exc, util as mapperutil
|
||||
from sqlalchemy.orm import exc, util as mapperutil, attributes
|
||||
|
||||
def populate(source, source_mapper, dest, dest_mapper,
|
||||
synchronize_pairs, uowcommit, flag_cascaded_pks):
|
||||
source_dict = source.dict
|
||||
dest_dict = dest.dict
|
||||
|
||||
for l, r in synchronize_pairs:
|
||||
try:
|
||||
value = source_mapper._get_state_attr_by_column(source, source.dict, l)
|
||||
# inline of source_mapper._get_state_attr_by_column
|
||||
prop = source_mapper._columntoproperty[l]
|
||||
value = source.manager[prop.key].impl.get(source, source_dict,
|
||||
attributes.PASSIVE_OFF)
|
||||
except exc.UnmappedColumnError:
|
||||
_raise_col_to_prop(False, source_mapper, l, dest_mapper, r)
|
||||
|
||||
try:
|
||||
dest_mapper._set_state_attr_by_column(dest, dest.dict, r, value)
|
||||
# inline of dest_mapper._set_state_attr_by_column
|
||||
prop = dest_mapper._columntoproperty[r]
|
||||
dest.manager[prop.key].impl.set(dest, dest_dict, value, None)
|
||||
except exc.UnmappedColumnError:
|
||||
_raise_col_to_prop(True, source_mapper, l, dest_mapper, r)
|
||||
|
||||
# techically the "r.primary_key" check isn't
|
||||
# technically the "r.primary_key" check isn't
|
||||
# needed here, but we check for this condition to limit
|
||||
# how often this logic is invoked for memory/performance
|
||||
# reasons, since we only need this info for a primary key
|
||||
# destination.
|
||||
if l.primary_key and r.primary_key and \
|
||||
r.references(l) and flag_cascaded_pks:
|
||||
if flag_cascaded_pks and l.primary_key and \
|
||||
r.primary_key and \
|
||||
r.references(l):
|
||||
uowcommit.attributes[("pk_cascaded", dest, r)] = True
|
||||
|
||||
def clear(dest, dest_mapper, synchronize_pairs):
|
||||
@@ -74,7 +83,8 @@ def source_modified(uowcommit, source, source_mapper, synchronize_pairs):
|
||||
prop = source_mapper._columntoproperty[l]
|
||||
except exc.UnmappedColumnError:
|
||||
_raise_col_to_prop(False, source_mapper, l, None, r)
|
||||
history = uowcommit.get_attribute_history(source, prop.key, passive=True)
|
||||
history = uowcommit.get_attribute_history(source, prop.key,
|
||||
attributes.PASSIVE_NO_INITIALIZE)
|
||||
return bool(history.deleted)
|
||||
else:
|
||||
return False
|
||||
@@ -85,7 +95,7 @@ def _raise_col_to_prop(isdest, source_mapper, source_column, dest_mapper, dest_c
|
||||
"Can't execute sync rule for destination column '%s'; "
|
||||
"mapper '%s' does not map this column. Try using an explicit"
|
||||
" `foreign_keys` collection which does not include this column "
|
||||
"(or use a viewonly=True relation)." % (dest_column, source_mapper)
|
||||
"(or use a viewonly=True relation)." % (dest_column, dest_mapper)
|
||||
)
|
||||
else:
|
||||
raise exc.UnmappedColumnError(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# orm/unitofwork.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 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
|
||||
@@ -12,46 +12,45 @@ organizes them in order of dependency, and executes.
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy import util, topological
|
||||
from sqlalchemy import util, event
|
||||
from sqlalchemy.util import topological
|
||||
from sqlalchemy.orm import attributes, interfaces
|
||||
from sqlalchemy.orm import util as mapperutil
|
||||
from sqlalchemy.orm.util import _state_mapper
|
||||
session = util.importlater("sqlalchemy.orm", "session")
|
||||
|
||||
class UOWEventHandler(interfaces.AttributeExtension):
|
||||
"""An event handler added to all relationship attributes which handles
|
||||
session cascade operations.
|
||||
def track_cascade_events(descriptor, prop):
|
||||
"""Establish event listeners on object attributes which handle
|
||||
cascade-on-set/append.
|
||||
|
||||
"""
|
||||
key = prop.key
|
||||
|
||||
active_history = False
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
def append(self, state, item, initiator):
|
||||
def append(state, item, initiator):
|
||||
# process "save_update" cascade rules for when
|
||||
# an instance is appended to the list of another instance
|
||||
|
||||
sess = session._state_session(state)
|
||||
if sess:
|
||||
prop = _state_mapper(state).get_property(self.key)
|
||||
prop = state.manager.mapper._props[key]
|
||||
item_state = attributes.instance_state(item)
|
||||
if prop.cascade.save_update and \
|
||||
(prop.cascade_backrefs or self.key == initiator.key) and \
|
||||
item not in sess:
|
||||
sess.add(item)
|
||||
(prop.cascade_backrefs or key == initiator.key) and \
|
||||
not sess._contains_state(item_state):
|
||||
sess._save_or_update_state(item_state)
|
||||
return item
|
||||
|
||||
def remove(self, state, item, initiator):
|
||||
def remove(state, item, initiator):
|
||||
sess = session._state_session(state)
|
||||
if sess:
|
||||
prop = _state_mapper(state).get_property(self.key)
|
||||
prop = state.manager.mapper._props[key]
|
||||
# expunge pending orphans
|
||||
item_state = attributes.instance_state(item)
|
||||
if prop.cascade.delete_orphan and \
|
||||
item in sess.new and \
|
||||
prop.mapper._is_orphan(attributes.instance_state(item)):
|
||||
item_state in sess._new and \
|
||||
prop.mapper._is_orphan(item_state):
|
||||
sess.expunge(item)
|
||||
|
||||
def set(self, state, newvalue, oldvalue, initiator):
|
||||
def set_(state, newvalue, oldvalue, initiator):
|
||||
# process "save_update" cascade rules for when an instance
|
||||
# is attached to another instance
|
||||
if oldvalue is newvalue:
|
||||
@@ -59,23 +58,33 @@ class UOWEventHandler(interfaces.AttributeExtension):
|
||||
|
||||
sess = session._state_session(state)
|
||||
if sess:
|
||||
prop = _state_mapper(state).get_property(self.key)
|
||||
if newvalue is not None and \
|
||||
prop.cascade.save_update and \
|
||||
(prop.cascade_backrefs or self.key == initiator.key) and \
|
||||
newvalue not in sess:
|
||||
sess.add(newvalue)
|
||||
if prop.cascade.delete_orphan and \
|
||||
oldvalue in sess.new and \
|
||||
prop.mapper._is_orphan(attributes.instance_state(oldvalue)):
|
||||
sess.expunge(oldvalue)
|
||||
prop = state.manager.mapper._props[key]
|
||||
if newvalue is not None:
|
||||
newvalue_state = attributes.instance_state(newvalue)
|
||||
if prop.cascade.save_update and \
|
||||
(prop.cascade_backrefs or key == initiator.key) and \
|
||||
not sess._contains_state(newvalue_state):
|
||||
sess._save_or_update_state(newvalue_state)
|
||||
|
||||
if oldvalue is not None and \
|
||||
oldvalue is not attributes.PASSIVE_NO_RESULT and \
|
||||
prop.cascade.delete_orphan:
|
||||
# possible to reach here with attributes.NEVER_SET ?
|
||||
oldvalue_state = attributes.instance_state(oldvalue)
|
||||
|
||||
if oldvalue_state in sess._new and \
|
||||
prop.mapper._is_orphan(oldvalue_state):
|
||||
sess.expunge(oldvalue)
|
||||
return newvalue
|
||||
|
||||
event.listen(descriptor, 'append', append, raw=True, retval=True)
|
||||
event.listen(descriptor, 'remove', remove, raw=True, retval=True)
|
||||
event.listen(descriptor, 'set', set_, raw=True, retval=True)
|
||||
|
||||
|
||||
class UOWTransaction(object):
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
self.mapper_flush_opts = session._mapper_flush_opts
|
||||
|
||||
# dictionary used by external actors to
|
||||
# store arbitrary state information.
|
||||
@@ -143,7 +152,8 @@ class UOWTransaction(object):
|
||||
|
||||
self.states[state] = (isdelete, True)
|
||||
|
||||
def get_attribute_history(self, state, key, passive=attributes.PASSIVE_NO_INITIALIZE):
|
||||
def get_attribute_history(self, state, key,
|
||||
passive=attributes.PASSIVE_NO_INITIALIZE):
|
||||
"""facade to attributes.get_state_history(), including caching of results."""
|
||||
|
||||
hashkey = ("history", state, key)
|
||||
@@ -151,21 +161,33 @@ class UOWTransaction(object):
|
||||
# cache the objects, not the states; the strong reference here
|
||||
# prevents newly loaded objects from being dereferenced during the
|
||||
# flush process
|
||||
if hashkey in self.attributes:
|
||||
(history, cached_passive) = self.attributes[hashkey]
|
||||
# if the cached lookup was "passive" and now we want non-passive, do a non-passive
|
||||
# lookup and re-cache
|
||||
if cached_passive and not passive:
|
||||
history = state.get_history(key, passive=False)
|
||||
self.attributes[hashkey] = (history, passive)
|
||||
else:
|
||||
history = state.get_history(key, passive=passive)
|
||||
self.attributes[hashkey] = (history, passive)
|
||||
|
||||
if not history or not state.get_impl(key).uses_objects:
|
||||
return history
|
||||
if hashkey in self.attributes:
|
||||
history, state_history, cached_passive = self.attributes[hashkey]
|
||||
# if the cached lookup was "passive" and now
|
||||
# we want non-passive, do a non-passive lookup and re-cache
|
||||
if cached_passive is not attributes.PASSIVE_OFF \
|
||||
and passive is attributes.PASSIVE_OFF:
|
||||
impl = state.manager[key].impl
|
||||
history = impl.get_history(state, state.dict,
|
||||
attributes.PASSIVE_OFF)
|
||||
if history and impl.uses_objects:
|
||||
state_history = history.as_state()
|
||||
else:
|
||||
state_history = history
|
||||
self.attributes[hashkey] = (history, state_history, passive)
|
||||
else:
|
||||
return history.as_state()
|
||||
impl = state.manager[key].impl
|
||||
# TODO: store the history as (state, object) tuples
|
||||
# so we don't have to keep converting here
|
||||
history = impl.get_history(state, state.dict, passive)
|
||||
if history and impl.uses_objects:
|
||||
state_history = history.as_state()
|
||||
else:
|
||||
state_history = history
|
||||
self.attributes[hashkey] = (history, state_history, passive)
|
||||
|
||||
return state_history
|
||||
|
||||
def has_dep(self, processor):
|
||||
return (processor, True) in self.presort_actions
|
||||
@@ -176,12 +198,17 @@ class UOWTransaction(object):
|
||||
self.presort_actions[key] = Preprocess(processor, fromparent)
|
||||
|
||||
def register_object(self, state, isdelete=False,
|
||||
listonly=False, cancel_delete=False):
|
||||
listonly=False, cancel_delete=False,
|
||||
operation=None, prop=None):
|
||||
if not self.session._contains_state(state):
|
||||
return
|
||||
if not state.deleted and operation is not None:
|
||||
util.warn("Object of type %s not in session, %s operation "
|
||||
"along '%s' will not proceed" %
|
||||
(mapperutil.state_class_str(state), operation, prop))
|
||||
return False
|
||||
|
||||
if state not in self.states:
|
||||
mapper = _state_mapper(state)
|
||||
mapper = state.manager.mapper
|
||||
|
||||
if mapper not in self.mappers:
|
||||
mapper._per_mapper_flush_actions(self)
|
||||
@@ -191,6 +218,7 @@ class UOWTransaction(object):
|
||||
else:
|
||||
if not listonly and (isdelete or cancel_delete):
|
||||
self.states[state] = (isdelete, False)
|
||||
return True
|
||||
|
||||
def issue_post_update(self, state, post_update_cols):
|
||||
mapper = state.manager.mapper.base_mapper
|
||||
@@ -283,9 +311,10 @@ class UOWTransaction(object):
|
||||
|
||||
#sort = topological.sort(self.dependencies, postsort_actions)
|
||||
#print "--------------"
|
||||
#print self.dependencies
|
||||
#print list(sort)
|
||||
#print "COUNT OF POSTSORT ACTIONS", len(postsort_actions)
|
||||
#print "\ndependencies:", self.dependencies
|
||||
#print "\ncycles:", self.cycles
|
||||
#print "\nsort:", list(sort)
|
||||
#print "\nCOUNT OF POSTSORT ACTIONS", len(postsort_actions)
|
||||
|
||||
# execute
|
||||
if self.cycles:
|
||||
|
||||
+187
-161
@@ -1,16 +1,16 @@
|
||||
# orm/util.py
|
||||
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2012 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
|
||||
|
||||
import sqlalchemy.exceptions as sa_exc
|
||||
from sqlalchemy import sql, util
|
||||
|
||||
from sqlalchemy import sql, util, event, exc as sa_exc
|
||||
from sqlalchemy.sql import expression, util as sql_util, operators
|
||||
from sqlalchemy.orm.interfaces import MapperExtension, EXT_CONTINUE,\
|
||||
PropComparator, MapperProperty,\
|
||||
AttributeExtension
|
||||
PropComparator, MapperProperty
|
||||
from sqlalchemy.orm import attributes, exc
|
||||
import operator
|
||||
|
||||
mapperlib = util.importlater("sqlalchemy.orm", "mapperlib")
|
||||
|
||||
@@ -20,7 +20,7 @@ all_cascades = frozenset(("delete", "delete-orphan", "all", "merge",
|
||||
|
||||
_INSTRUMENTOR = ('mapper', 'instrumentor')
|
||||
|
||||
class CascadeOptions(object):
|
||||
class CascadeOptions(dict):
|
||||
"""Keeps track of the options sent to relationship().cascade"""
|
||||
|
||||
def __init__(self, arg=""):
|
||||
@@ -28,69 +28,63 @@ class CascadeOptions(object):
|
||||
values = set()
|
||||
else:
|
||||
values = set(c.strip() for c in arg.split(','))
|
||||
|
||||
for name in ['save-update', 'delete', 'refresh-expire',
|
||||
'merge', 'expunge']:
|
||||
boolean = name in values or 'all' in values
|
||||
setattr(self, name.replace('-', '_'), boolean)
|
||||
if boolean:
|
||||
self[name] = True
|
||||
self.delete_orphan = "delete-orphan" in values
|
||||
self.delete = "delete" in values or "all" in values
|
||||
self.save_update = "save-update" in values or "all" in values
|
||||
self.merge = "merge" in values or "all" in values
|
||||
self.expunge = "expunge" in values or "all" in values
|
||||
self.refresh_expire = "refresh-expire" in values or "all" in values
|
||||
if self.delete_orphan:
|
||||
self['delete-orphan'] = True
|
||||
|
||||
if self.delete_orphan and not self.delete:
|
||||
util.warn("The 'delete-orphan' cascade option requires "
|
||||
"'delete'. This will raise an error in 0.6.")
|
||||
"'delete'.")
|
||||
|
||||
for x in values:
|
||||
if x not in all_cascades:
|
||||
raise sa_exc.ArgumentError("Invalid cascade option '%s'" % x)
|
||||
|
||||
def __contains__(self, item):
|
||||
return getattr(self, item.replace("-", "_"), False)
|
||||
|
||||
def __repr__(self):
|
||||
return "CascadeOptions(%s)" % repr(",".join(
|
||||
[x for x in ['delete', 'save_update', 'merge', 'expunge',
|
||||
'delete_orphan', 'refresh-expire']
|
||||
if getattr(self, x, False) is True]))
|
||||
|
||||
def _validator_events(desc, key, validator):
|
||||
"""Runs a validation method on an attribute value to be set or appended."""
|
||||
|
||||
class Validator(AttributeExtension):
|
||||
"""Runs a validation method on an attribute value to be set or appended.
|
||||
def append(state, value, initiator):
|
||||
return validator(state.obj(), key, value)
|
||||
|
||||
The Validator class is used by the :func:`~sqlalchemy.orm.validates`
|
||||
decorator, and direct access is usually not needed.
|
||||
def set_(state, value, oldvalue, initiator):
|
||||
return validator(state.obj(), key, value)
|
||||
|
||||
"""
|
||||
event.listen(desc, 'append', append, raw=True, retval=True)
|
||||
event.listen(desc, 'set', set_, raw=True, retval=True)
|
||||
|
||||
def __init__(self, key, validator):
|
||||
"""Construct a new Validator.
|
||||
|
||||
key - name of the attribute to be validated;
|
||||
will be passed as the second argument to
|
||||
the validation method (the first is the object instance itself).
|
||||
|
||||
validator - an function or instance method which accepts
|
||||
three arguments; an instance (usually just 'self' for a method),
|
||||
the key name of the attribute, and the value. The function should
|
||||
return the same value given, unless it wishes to modify it.
|
||||
|
||||
"""
|
||||
self.key = key
|
||||
self.validator = validator
|
||||
|
||||
def append(self, state, value, initiator):
|
||||
return self.validator(state.obj(), self.key, value)
|
||||
|
||||
def set(self, state, value, oldvalue, initiator):
|
||||
return self.validator(state.obj(), self.key, value)
|
||||
|
||||
def polymorphic_union(table_map, typecolname, aliasname='p_union'):
|
||||
def polymorphic_union(table_map, typecolname, aliasname='p_union', cast_nulls=True):
|
||||
"""Create a ``UNION`` statement used by a polymorphic mapper.
|
||||
|
||||
See :ref:`concrete_inheritance` for an example of how
|
||||
this is used.
|
||||
|
||||
:param table_map: mapping of polymorphic identities to
|
||||
:class:`.Table` objects.
|
||||
:param typecolname: string name of a "discriminator" column, which will be
|
||||
derived from the query, producing the polymorphic identity for each row. If
|
||||
``None``, no polymorphic discriminator is generated.
|
||||
:param aliasname: name of the :func:`~sqlalchemy.sql.expression.alias()`
|
||||
construct generated.
|
||||
:param cast_nulls: if True, non-existent columns, which are represented as labeled
|
||||
NULLs, will be passed into CAST. This is a legacy behavior that is problematic
|
||||
on some backends such as Oracle - in which case it can be set to False.
|
||||
|
||||
"""
|
||||
|
||||
colnames = set()
|
||||
colnames = util.OrderedSet()
|
||||
colnamemaps = {}
|
||||
types = {}
|
||||
for key in table_map.keys():
|
||||
@@ -113,7 +107,10 @@ def polymorphic_union(table_map, typecolname, aliasname='p_union'):
|
||||
try:
|
||||
return colnamemaps[table][name]
|
||||
except KeyError:
|
||||
return sql.cast(sql.null(), types[name]).label(name)
|
||||
if cast_nulls:
|
||||
return sql.cast(sql.null(), types[name]).label(name)
|
||||
else:
|
||||
return sql.type_coerce(sql.null(), types[name]).label(name)
|
||||
|
||||
result = []
|
||||
for type, table in table_map.iteritems():
|
||||
@@ -184,78 +181,6 @@ def identity_key(*args, **kwargs):
|
||||
mapper = object_mapper(instance)
|
||||
return mapper.identity_key_from_instance(instance)
|
||||
|
||||
class ExtensionCarrier(dict):
|
||||
"""Fronts an ordered collection of MapperExtension objects.
|
||||
|
||||
Bundles multiple MapperExtensions into a unified callable unit,
|
||||
encapsulating ordering, looping and EXT_CONTINUE logic. The
|
||||
ExtensionCarrier implements the MapperExtension interface, e.g.::
|
||||
|
||||
carrier.after_insert(...args...)
|
||||
|
||||
The dictionary interface provides containment for implemented
|
||||
method names mapped to a callable which executes that method
|
||||
for participating extensions.
|
||||
|
||||
"""
|
||||
|
||||
interface = set(method for method in dir(MapperExtension)
|
||||
if not method.startswith('_'))
|
||||
|
||||
def __init__(self, extensions=None):
|
||||
self._extensions = []
|
||||
for ext in extensions or ():
|
||||
self.append(ext)
|
||||
|
||||
def copy(self):
|
||||
return ExtensionCarrier(self._extensions)
|
||||
|
||||
def push(self, extension):
|
||||
"""Insert a MapperExtension at the beginning of the collection."""
|
||||
self._register(extension)
|
||||
self._extensions.insert(0, extension)
|
||||
|
||||
def append(self, extension):
|
||||
"""Append a MapperExtension at the end of the collection."""
|
||||
self._register(extension)
|
||||
self._extensions.append(extension)
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over MapperExtensions in the collection."""
|
||||
return iter(self._extensions)
|
||||
|
||||
def _register(self, extension):
|
||||
"""Register callable fronts for overridden interface methods."""
|
||||
|
||||
for method in self.interface.difference(self):
|
||||
impl = getattr(extension, method, None)
|
||||
if impl and impl is not getattr(MapperExtension, method):
|
||||
self[method] = self._create_do(method)
|
||||
|
||||
def _create_do(self, method):
|
||||
"""Return a closure that loops over impls of the named method."""
|
||||
|
||||
def _do(*args, **kwargs):
|
||||
for ext in self._extensions:
|
||||
ret = getattr(ext, method)(*args, **kwargs)
|
||||
if ret is not EXT_CONTINUE:
|
||||
return ret
|
||||
else:
|
||||
return EXT_CONTINUE
|
||||
_do.__name__ = method
|
||||
return _do
|
||||
|
||||
@staticmethod
|
||||
def _pass(*args, **kwargs):
|
||||
return EXT_CONTINUE
|
||||
|
||||
def __getattr__(self, key):
|
||||
"""Delegate MapperExtension methods to bundled fronts."""
|
||||
|
||||
if key not in self.interface:
|
||||
raise AttributeError(key)
|
||||
return self.get(key, self._pass)
|
||||
|
||||
class ORMAdapter(sql_util.ColumnAdapter):
|
||||
"""Extends ColumnAdapter to accept ORM entities.
|
||||
|
||||
@@ -296,15 +221,61 @@ class AliasedClass(object):
|
||||
session.query(User, user_alias).\\
|
||||
join((user_alias, User.id > user_alias.id)).\\
|
||||
filter(User.name==user_alias.name)
|
||||
|
||||
|
||||
The resulting object is an instance of :class:`.AliasedClass`, however
|
||||
it implements a ``__getattribute__()`` scheme which will proxy attribute
|
||||
access to that of the ORM class being aliased. All classmethods
|
||||
on the mapped entity should also be available here, including
|
||||
hybrids created with the :ref:`hybrids_toplevel` extension,
|
||||
which will receive the :class:`.AliasedClass` as the "class" argument
|
||||
when classmethods are called.
|
||||
|
||||
:param cls: ORM mapped entity which will be "wrapped" around an alias.
|
||||
:param alias: a selectable, such as an :func:`.alias` or :func:`.select`
|
||||
construct, which will be rendered in place of the mapped table of the
|
||||
ORM entity. If left as ``None``, an ordinary :class:`.Alias` of the
|
||||
ORM entity's mapped table will be generated.
|
||||
:param name: A name which will be applied both to the :class:`.Alias`
|
||||
if one is generated, as well as the name present in the "named tuple"
|
||||
returned by the :class:`.Query` object when results are returned.
|
||||
:param adapt_on_names: if True, more liberal "matching" will be used when
|
||||
mapping the mapped columns of the ORM entity to those of the given selectable -
|
||||
a name-based match will be performed if the given selectable doesn't
|
||||
otherwise have a column that corresponds to one on the entity. The
|
||||
use case for this is when associating an entity with some derived
|
||||
selectable such as one that uses aggregate functions::
|
||||
|
||||
class UnitPrice(Base):
|
||||
__tablename__ = 'unit_price'
|
||||
...
|
||||
unit_id = Column(Integer)
|
||||
price = Column(Numeric)
|
||||
|
||||
aggregated_unit_price = Session.query(
|
||||
func.sum(UnitPrice.price).label('price')
|
||||
).group_by(UnitPrice.unit_id).subquery()
|
||||
|
||||
aggregated_unit_price = aliased(UnitPrice, alias=aggregated_unit_price, adapt_on_names=True)
|
||||
|
||||
Above, functions on ``aggregated_unit_price`` which
|
||||
refer to ``.price`` will return the
|
||||
``fund.sum(UnitPrice.price).label('price')`` column,
|
||||
as it is matched on the name "price". Ordinarily, the "price" function wouldn't
|
||||
have any "column correspondence" to the actual ``UnitPrice.price`` column
|
||||
as it is not a proxy of the original.
|
||||
|
||||
``adapt_on_names`` is new in 0.7.3.
|
||||
|
||||
"""
|
||||
def __init__(self, cls, alias=None, name=None):
|
||||
def __init__(self, cls, alias=None, name=None, adapt_on_names=False):
|
||||
self.__mapper = _class_to_mapper(cls)
|
||||
self.__target = self.__mapper.class_
|
||||
self.__adapt_on_names = adapt_on_names
|
||||
if alias is None:
|
||||
alias = self.__mapper._with_polymorphic_selectable.alias()
|
||||
alias = self.__mapper._with_polymorphic_selectable.alias(name=name)
|
||||
self.__adapter = sql_util.ClauseAdapter(alias,
|
||||
equivalents=self.__mapper._equivalent_columns)
|
||||
equivalents=self.__mapper._equivalent_columns,
|
||||
adapt_on_names=self.__adapt_on_names)
|
||||
self.__alias = alias
|
||||
# used to assign a name to the RowTuple object
|
||||
# returned by Query.
|
||||
@@ -315,15 +286,18 @@ class AliasedClass(object):
|
||||
return {
|
||||
'mapper':self.__mapper,
|
||||
'alias':self.__alias,
|
||||
'name':self._sa_label_name
|
||||
'name':self._sa_label_name,
|
||||
'adapt_on_names':self.__adapt_on_names,
|
||||
}
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__mapper = state['mapper']
|
||||
self.__target = self.__mapper.class_
|
||||
self.__adapt_on_names = state['adapt_on_names']
|
||||
alias = state['alias']
|
||||
self.__adapter = sql_util.ClauseAdapter(alias,
|
||||
equivalents=self.__mapper._equivalent_columns)
|
||||
equivalents=self.__mapper._equivalent_columns,
|
||||
adapt_on_names=self.__adapt_on_names)
|
||||
self.__alias = alias
|
||||
name = state['name']
|
||||
self._sa_label_name = name
|
||||
@@ -336,23 +310,15 @@ class AliasedClass(object):
|
||||
'parentmapper':self.__mapper}
|
||||
)
|
||||
|
||||
def __adapt_prop(self, prop):
|
||||
existing = getattr(self.__target, prop.key)
|
||||
def __adapt_prop(self, existing, key):
|
||||
comparator = existing.comparator.adapted(self.__adapt_element)
|
||||
|
||||
queryattr = attributes.QueryableAttribute(prop.key,
|
||||
queryattr = attributes.QueryableAttribute(self, key,
|
||||
impl=existing.impl, parententity=self, comparator=comparator)
|
||||
setattr(self, prop.key, queryattr)
|
||||
setattr(self, key, queryattr)
|
||||
return queryattr
|
||||
|
||||
def __getattr__(self, key):
|
||||
if self.__mapper.has_property(key):
|
||||
return self.__adapt_prop(
|
||||
self.__mapper.get_property(
|
||||
key, _compile_mappers=False
|
||||
)
|
||||
)
|
||||
|
||||
for base in self.__target.__mro__:
|
||||
try:
|
||||
attr = object.__getattribute__(base, key)
|
||||
@@ -363,14 +329,19 @@ class AliasedClass(object):
|
||||
else:
|
||||
raise AttributeError(key)
|
||||
|
||||
if hasattr(attr, 'func_code'):
|
||||
if isinstance(attr, attributes.QueryableAttribute):
|
||||
return self.__adapt_prop(attr, key)
|
||||
elif hasattr(attr, 'func_code'):
|
||||
is_method = getattr(self.__target, key, None)
|
||||
if is_method and is_method.im_self is not None:
|
||||
return util.types.MethodType(attr.im_func, self, self)
|
||||
else:
|
||||
return None
|
||||
elif hasattr(attr, '__get__'):
|
||||
return attr.__get__(None, self)
|
||||
ret = attr.__get__(None, self)
|
||||
if isinstance(ret, PropComparator):
|
||||
return ret.adapted(self.__adapt_element)
|
||||
return ret
|
||||
else:
|
||||
return attr
|
||||
|
||||
@@ -378,6 +349,14 @@ class AliasedClass(object):
|
||||
return '<AliasedClass at 0x%x; %s>' % (
|
||||
id(self), self.__target.__name__)
|
||||
|
||||
def aliased(element, alias=None, name=None, adapt_on_names=False):
|
||||
if isinstance(element, expression.FromClause):
|
||||
if adapt_on_names:
|
||||
raise sa_exc.ArgumentError("adapt_on_names only applies to ORM elements")
|
||||
return element.alias(name)
|
||||
else:
|
||||
return AliasedClass(element, alias=alias, name=name, adapt_on_names=adapt_on_names)
|
||||
|
||||
def _orm_annotate(element, exclude=None):
|
||||
"""Deep copy the given ClauseElement, annotating each element with the
|
||||
"_orm_adapt" flag.
|
||||
@@ -453,29 +432,52 @@ class _ORMJoin(expression.Join):
|
||||
|
||||
def join(left, right, onclause=None, isouter=False, join_to_left=True):
|
||||
"""Produce an inner join between left and right clauses.
|
||||
|
||||
:func:`.orm.join` is an extension to the core join interface
|
||||
provided by :func:`.sql.expression.join()`, where the
|
||||
left and right selectables may be not only core selectable
|
||||
objects such as :class:`.Table`, but also mapped classes or
|
||||
:class:`.AliasedClass` instances. The "on" clause can
|
||||
be a SQL expression, or an attribute or string name
|
||||
referencing a configured :func:`.relationship`.
|
||||
|
||||
In addition to the interface provided by
|
||||
:func:`~sqlalchemy.sql.expression.join()`, left and right may be mapped
|
||||
classes or AliasedClass instances. The onclause may be a
|
||||
string name of a relationship(), or a class-bound descriptor
|
||||
representing a relationship.
|
||||
|
||||
join_to_left indicates to attempt aliasing the ON clause,
|
||||
``join_to_left`` indicates to attempt aliasing the ON clause,
|
||||
in whatever form it is passed, to the selectable
|
||||
passed as the left side. If False, the onclause
|
||||
is used as is.
|
||||
|
||||
:func:`.orm.join` is not commonly needed in modern usage,
|
||||
as its functionality is encapsulated within that of the
|
||||
:meth:`.Query.join` method, which features a
|
||||
significant amount of automation beyond :func:`.orm.join`
|
||||
by itself. Explicit usage of :func:`.orm.join`
|
||||
with :class:`.Query` involves usage of the
|
||||
:meth:`.Query.select_from` method, as in::
|
||||
|
||||
from sqlalchemy.orm import join
|
||||
session.query(User).\\
|
||||
select_from(join(User, Address, User.addresses)).\\
|
||||
filter(Address.email_address=='foo@bar.com')
|
||||
|
||||
In modern SQLAlchemy the above join can be written more
|
||||
succinctly as::
|
||||
|
||||
session.query(User).\\
|
||||
join(User.addresses).\\
|
||||
filter(Address.email_address=='foo@bar.com')
|
||||
|
||||
See :meth:`.Query.join` for information on modern usage
|
||||
of ORM level joins.
|
||||
|
||||
"""
|
||||
return _ORMJoin(left, right, onclause, isouter, join_to_left)
|
||||
|
||||
def outerjoin(left, right, onclause=None, join_to_left=True):
|
||||
"""Produce a left outer join between left and right clauses.
|
||||
|
||||
In addition to the interface provided by
|
||||
:func:`~sqlalchemy.sql.expression.outerjoin()`, left and right may be
|
||||
mapped classes or AliasedClass instances. The onclause may be a string
|
||||
name of a relationship(), or a class-bound descriptor representing a
|
||||
relationship.
|
||||
This is the "outer join" version of the :func:`.orm.join` function,
|
||||
featuring the same behavior except that an OUTER JOIN is generated.
|
||||
See that function's documentation for other usage details.
|
||||
|
||||
"""
|
||||
return _ORMJoin(left, right, onclause, True, join_to_left)
|
||||
@@ -508,7 +510,7 @@ def with_parent(instance, prop):
|
||||
"""
|
||||
if isinstance(prop, basestring):
|
||||
mapper = object_mapper(instance)
|
||||
prop = mapper.get_property(prop, resolve_synonyms=True)
|
||||
prop = getattr(mapper.class_, prop).property
|
||||
elif isinstance(prop, attributes.QueryableAttribute):
|
||||
prop = prop.property
|
||||
|
||||
@@ -544,8 +546,8 @@ def _entity_info(entity, compile=True):
|
||||
else:
|
||||
return None, entity, False
|
||||
|
||||
if compile:
|
||||
mapper = mapper.compile()
|
||||
if compile and mapperlib.module._new_mappers:
|
||||
mapperlib.configure_mappers()
|
||||
return mapper, mapper._with_polymorphic_selectable, False
|
||||
|
||||
def _entity_descriptor(entity, key):
|
||||
@@ -586,8 +588,7 @@ def _attr_as_key(attr):
|
||||
def _is_aliased_class(entity):
|
||||
return isinstance(entity, AliasedClass)
|
||||
|
||||
def _state_mapper(state):
|
||||
return state.manager.mapper
|
||||
_state_mapper = util.dottedgetter('manager.mapper')
|
||||
|
||||
def object_mapper(instance):
|
||||
"""Given an object, return the primary Mapper associated with the object
|
||||
@@ -605,9 +606,12 @@ def object_mapper(instance):
|
||||
raise exc.UnmappedInstanceError(instance)
|
||||
|
||||
def class_mapper(class_, compile=True):
|
||||
"""Given a class, return the primary Mapper associated with the key.
|
||||
"""Given a class, return the primary :class:`.Mapper` associated
|
||||
with the key.
|
||||
|
||||
Raises UnmappedClassError if no mapping is configured.
|
||||
Raises :class:`.UnmappedClassError` if no mapping is configured
|
||||
on the given class, or :class:`.ArgumentError` if a non-class
|
||||
object is passed.
|
||||
|
||||
"""
|
||||
|
||||
@@ -616,10 +620,12 @@ def class_mapper(class_, compile=True):
|
||||
mapper = class_manager.mapper
|
||||
|
||||
except exc.NO_STATE:
|
||||
if not isinstance(class_, type):
|
||||
raise sa_exc.ArgumentError("Class object expected, got '%r'." % class_)
|
||||
raise exc.UnmappedClassError(class_)
|
||||
|
||||
if compile:
|
||||
mapper = mapper.compile()
|
||||
if compile and mapperlib.module._new_mappers:
|
||||
mapperlib.configure_mappers()
|
||||
return mapper
|
||||
|
||||
def _class_to_mapper(class_or_mapper, compile=True):
|
||||
@@ -637,16 +643,18 @@ def _class_to_mapper(class_or_mapper, compile=True):
|
||||
else:
|
||||
raise exc.UnmappedClassError(class_or_mapper)
|
||||
|
||||
if compile:
|
||||
return mapper.compile()
|
||||
else:
|
||||
return mapper
|
||||
if compile and mapperlib.module._new_mappers:
|
||||
mapperlib.configure_mappers()
|
||||
return mapper
|
||||
|
||||
def has_identity(object):
|
||||
state = attributes.instance_state(object)
|
||||
return state.has_identity
|
||||
|
||||
def _is_mapped_class(cls):
|
||||
"""Return True if the given object is a mapped class,
|
||||
:class:`.Mapper`, or :class:`.AliasedClass`."""
|
||||
|
||||
if isinstance(cls, (AliasedClass, mapperlib.Mapper)):
|
||||
return True
|
||||
if isinstance(cls, expression.ClauseElement):
|
||||
@@ -656,6 +664,16 @@ def _is_mapped_class(cls):
|
||||
return manager and _INSTRUMENTOR in manager.info
|
||||
return False
|
||||
|
||||
def _mapper_or_none(cls):
|
||||
"""Return the :class:`.Mapper` for the given class or None if the
|
||||
class is not mapped."""
|
||||
|
||||
manager = attributes.manager_of_class(cls)
|
||||
if manager is not None and _INSTRUMENTOR in manager.info:
|
||||
return manager.info[_INSTRUMENTOR]
|
||||
else:
|
||||
return None
|
||||
|
||||
def instance_str(instance):
|
||||
"""Return a string describing an instance."""
|
||||
|
||||
@@ -669,6 +687,14 @@ def state_str(state):
|
||||
else:
|
||||
return '<%s at 0x%x>' % (state.class_.__name__, id(state.obj()))
|
||||
|
||||
def state_class_str(state):
|
||||
"""Return a string describing an instance's class via its InstanceState."""
|
||||
|
||||
if state is None:
|
||||
return "None"
|
||||
else:
|
||||
return '<%s>' % (state.class_.__name__, )
|
||||
|
||||
def attribute_str(instance, attribute):
|
||||
return instance_str(instance) + "." + attribute
|
||||
|
||||
|
||||
Reference in New Issue
Block a user