Packages update

This commit is contained in:
Ruud
2012-02-11 16:28:06 +01:00
parent 3bbf1126c3
commit 02e01fb2d6
217 changed files with 26395 additions and 21194 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+31 -25
View File
@@ -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
+145 -79
View File
@@ -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
+422
View File
@@ -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)
+46 -35
View File
@@ -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 -1
View File
@@ -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
+42 -4
View File
@@ -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."""
+71 -89
View File
@@ -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."""
+674
View File
@@ -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
View File
@@ -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')
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+10 -92
View File
@@ -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')
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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
View File
@@ -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)
File diff suppressed because it is too large Load Diff
+19 -9
View File
@@ -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(
+81 -52
View File
@@ -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
View File
@@ -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