Remove Elixir library
Update SQLAlchemy
This commit is contained in:
+92
-1510
File diff suppressed because it is too large
Load Diff
+392
-266
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,453 @@
|
||||
# orm/base.py
|
||||
# Copyright (C) 2005-2014 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
|
||||
|
||||
"""Constants and rudimental functions used throughout the ORM.
|
||||
|
||||
"""
|
||||
|
||||
from .. import util, inspection, exc as sa_exc
|
||||
from ..sql import expression
|
||||
from . import exc
|
||||
import operator
|
||||
|
||||
PASSIVE_NO_RESULT = util.symbol('PASSIVE_NO_RESULT',
|
||||
"""Symbol returned by a loader callable or other attribute/history
|
||||
retrieval operation when a value could not be determined, based
|
||||
on loader callable flags.
|
||||
"""
|
||||
)
|
||||
|
||||
ATTR_WAS_SET = util.symbol('ATTR_WAS_SET',
|
||||
"""Symbol returned by a loader callable to indicate the
|
||||
retrieved value, or values, were assigned to their attributes
|
||||
on the target object.
|
||||
""")
|
||||
|
||||
ATTR_EMPTY = util.symbol('ATTR_EMPTY',
|
||||
"""Symbol used internally to indicate an attribute had no callable.
|
||||
""")
|
||||
|
||||
NO_VALUE = util.symbol('NO_VALUE',
|
||||
"""Symbol which may be placed as the 'previous' value of an attribute,
|
||||
indicating no value was loaded for an attribute when it was modified,
|
||||
and flags indicated we were not to load it.
|
||||
"""
|
||||
)
|
||||
|
||||
NEVER_SET = util.symbol('NEVER_SET',
|
||||
"""Symbol which may be placed as the 'previous' value of an attribute
|
||||
indicating that the attribute had not been assigned to previously.
|
||||
"""
|
||||
)
|
||||
|
||||
NO_CHANGE = util.symbol("NO_CHANGE",
|
||||
"""No callables or SQL should be emitted on attribute access
|
||||
and no state should change""", canonical=0
|
||||
)
|
||||
|
||||
CALLABLES_OK = util.symbol("CALLABLES_OK",
|
||||
"""Loader callables can be fired off if a value
|
||||
is not present.""", canonical=1
|
||||
)
|
||||
|
||||
SQL_OK = util.symbol("SQL_OK",
|
||||
"""Loader callables can emit SQL at least on scalar value
|
||||
attributes.""", canonical=2)
|
||||
|
||||
RELATED_OBJECT_OK = util.symbol("RELATED_OBJECT_OK",
|
||||
"""callables can use SQL to load related objects as well
|
||||
as scalar value attributes.
|
||||
""", canonical=4
|
||||
)
|
||||
|
||||
INIT_OK = util.symbol("INIT_OK",
|
||||
"""Attributes should be initialized with a blank
|
||||
value (None or an empty collection) upon get, if no other
|
||||
value can be obtained.
|
||||
""", canonical=8
|
||||
)
|
||||
|
||||
NON_PERSISTENT_OK = util.symbol("NON_PERSISTENT_OK",
|
||||
"""callables can be emitted if the parent is not persistent.""",
|
||||
canonical=16
|
||||
)
|
||||
|
||||
LOAD_AGAINST_COMMITTED = util.symbol("LOAD_AGAINST_COMMITTED",
|
||||
"""callables should use committed values as primary/foreign keys during a load
|
||||
""", canonical=32
|
||||
)
|
||||
|
||||
# pre-packaged sets of flags used as inputs
|
||||
PASSIVE_OFF = util.symbol("PASSIVE_OFF",
|
||||
"Callables can be emitted in all cases.",
|
||||
canonical=(RELATED_OBJECT_OK | NON_PERSISTENT_OK |
|
||||
INIT_OK | CALLABLES_OK | SQL_OK)
|
||||
)
|
||||
PASSIVE_RETURN_NEVER_SET = util.symbol("PASSIVE_RETURN_NEVER_SET",
|
||||
"""PASSIVE_OFF ^ INIT_OK""",
|
||||
canonical=PASSIVE_OFF ^ INIT_OK
|
||||
)
|
||||
PASSIVE_NO_INITIALIZE = util.symbol("PASSIVE_NO_INITIALIZE",
|
||||
"PASSIVE_RETURN_NEVER_SET ^ CALLABLES_OK",
|
||||
canonical=PASSIVE_RETURN_NEVER_SET ^ CALLABLES_OK
|
||||
)
|
||||
PASSIVE_NO_FETCH = util.symbol("PASSIVE_NO_FETCH",
|
||||
"PASSIVE_OFF ^ SQL_OK",
|
||||
canonical=PASSIVE_OFF ^ SQL_OK
|
||||
)
|
||||
PASSIVE_NO_FETCH_RELATED = util.symbol("PASSIVE_NO_FETCH_RELATED",
|
||||
"PASSIVE_OFF ^ RELATED_OBJECT_OK",
|
||||
canonical=PASSIVE_OFF ^ RELATED_OBJECT_OK
|
||||
)
|
||||
PASSIVE_ONLY_PERSISTENT = util.symbol("PASSIVE_ONLY_PERSISTENT",
|
||||
"PASSIVE_OFF ^ NON_PERSISTENT_OK",
|
||||
canonical=PASSIVE_OFF ^ NON_PERSISTENT_OK
|
||||
)
|
||||
|
||||
DEFAULT_MANAGER_ATTR = '_sa_class_manager'
|
||||
DEFAULT_STATE_ATTR = '_sa_instance_state'
|
||||
_INSTRUMENTOR = ('mapper', 'instrumentor')
|
||||
|
||||
EXT_CONTINUE = util.symbol('EXT_CONTINUE')
|
||||
EXT_STOP = util.symbol('EXT_STOP')
|
||||
|
||||
ONETOMANY = util.symbol('ONETOMANY',
|
||||
"""Indicates the one-to-many direction for a :func:`.relationship`.
|
||||
|
||||
This symbol is typically used by the internals but may be exposed within
|
||||
certain API features.
|
||||
|
||||
""")
|
||||
|
||||
MANYTOONE = util.symbol('MANYTOONE',
|
||||
"""Indicates the many-to-one direction for a :func:`.relationship`.
|
||||
|
||||
This symbol is typically used by the internals but may be exposed within
|
||||
certain API features.
|
||||
|
||||
""")
|
||||
|
||||
MANYTOMANY = util.symbol('MANYTOMANY',
|
||||
"""Indicates the many-to-many direction for a :func:`.relationship`.
|
||||
|
||||
This symbol is typically used by the internals but may be exposed within
|
||||
certain API features.
|
||||
|
||||
""")
|
||||
|
||||
NOT_EXTENSION = util.symbol('NOT_EXTENSION',
|
||||
"""Symbol indicating an :class:`_InspectionAttr` that's
|
||||
not part of sqlalchemy.ext.
|
||||
|
||||
Is assigned to the :attr:`._InspectionAttr.extension_type`
|
||||
attibute.
|
||||
|
||||
""")
|
||||
|
||||
_none_set = frozenset([None])
|
||||
|
||||
|
||||
def _generative(*assertions):
|
||||
"""Mark a method as generative, e.g. method-chained."""
|
||||
|
||||
@util.decorator
|
||||
def generate(fn, *args, **kw):
|
||||
self = args[0]._clone()
|
||||
for assertion in assertions:
|
||||
assertion(self, fn.__name__)
|
||||
fn(self, *args[1:], **kw)
|
||||
return self
|
||||
return generate
|
||||
|
||||
|
||||
# these can be replaced by sqlalchemy.ext.instrumentation
|
||||
# if augmented class instrumentation is enabled.
|
||||
def manager_of_class(cls):
|
||||
return cls.__dict__.get(DEFAULT_MANAGER_ATTR, None)
|
||||
|
||||
instance_state = operator.attrgetter(DEFAULT_STATE_ATTR)
|
||||
|
||||
instance_dict = operator.attrgetter('__dict__')
|
||||
|
||||
def instance_str(instance):
|
||||
"""Return a string describing an instance."""
|
||||
|
||||
return state_str(instance_state(instance))
|
||||
|
||||
def state_str(state):
|
||||
"""Return a string describing an instance via its InstanceState."""
|
||||
|
||||
if state is None:
|
||||
return "None"
|
||||
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
|
||||
|
||||
|
||||
def state_attribute_str(state, attribute):
|
||||
return state_str(state) + "." + attribute
|
||||
|
||||
def object_mapper(instance):
|
||||
"""Given an object, return the primary Mapper associated with the object
|
||||
instance.
|
||||
|
||||
Raises :class:`sqlalchemy.orm.exc.UnmappedInstanceError`
|
||||
if no mapping is configured.
|
||||
|
||||
This function is available via the inspection system as::
|
||||
|
||||
inspect(instance).mapper
|
||||
|
||||
Using the inspection system will raise
|
||||
:class:`sqlalchemy.exc.NoInspectionAvailable` if the instance is
|
||||
not part of a mapping.
|
||||
|
||||
"""
|
||||
return object_state(instance).mapper
|
||||
|
||||
|
||||
def object_state(instance):
|
||||
"""Given an object, return the :class:`.InstanceState`
|
||||
associated with the object.
|
||||
|
||||
Raises :class:`sqlalchemy.orm.exc.UnmappedInstanceError`
|
||||
if no mapping is configured.
|
||||
|
||||
Equivalent functionality is available via the :func:`.inspect`
|
||||
function as::
|
||||
|
||||
inspect(instance)
|
||||
|
||||
Using the inspection system will raise
|
||||
:class:`sqlalchemy.exc.NoInspectionAvailable` if the instance is
|
||||
not part of a mapping.
|
||||
|
||||
"""
|
||||
state = _inspect_mapped_object(instance)
|
||||
if state is None:
|
||||
raise exc.UnmappedInstanceError(instance)
|
||||
else:
|
||||
return state
|
||||
|
||||
|
||||
@inspection._inspects(object)
|
||||
def _inspect_mapped_object(instance):
|
||||
try:
|
||||
return instance_state(instance)
|
||||
# TODO: whats the py-2/3 syntax to catch two
|
||||
# different kinds of exceptions at once ?
|
||||
except exc.UnmappedClassError:
|
||||
return None
|
||||
except exc.NO_STATE:
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def _class_to_mapper(class_or_mapper):
|
||||
insp = inspection.inspect(class_or_mapper, False)
|
||||
if insp is not None:
|
||||
return insp.mapper
|
||||
else:
|
||||
raise exc.UnmappedClassError(class_or_mapper)
|
||||
|
||||
|
||||
def _mapper_or_none(entity):
|
||||
"""Return the :class:`.Mapper` for the given class or None if the
|
||||
class is not mapped."""
|
||||
|
||||
insp = inspection.inspect(entity, False)
|
||||
if insp is not None:
|
||||
return insp.mapper
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _is_mapped_class(entity):
|
||||
"""Return True if the given object is a mapped class,
|
||||
:class:`.Mapper`, or :class:`.AliasedClass`."""
|
||||
|
||||
insp = inspection.inspect(entity, False)
|
||||
return insp is not None and \
|
||||
hasattr(insp, "mapper") and \
|
||||
(
|
||||
insp.is_mapper
|
||||
or insp.is_aliased_class
|
||||
)
|
||||
|
||||
def _attr_as_key(attr):
|
||||
if hasattr(attr, 'key'):
|
||||
return attr.key
|
||||
else:
|
||||
return expression._column_as_key(attr)
|
||||
|
||||
|
||||
|
||||
def _orm_columns(entity):
|
||||
insp = inspection.inspect(entity, False)
|
||||
if hasattr(insp, 'selectable'):
|
||||
return [c for c in insp.selectable.c]
|
||||
else:
|
||||
return [entity]
|
||||
|
||||
|
||||
|
||||
def _is_aliased_class(entity):
|
||||
insp = inspection.inspect(entity, False)
|
||||
return insp is not None and \
|
||||
getattr(insp, "is_aliased_class", False)
|
||||
|
||||
|
||||
def _entity_descriptor(entity, key):
|
||||
"""Return a class attribute given an entity and string name.
|
||||
|
||||
May return :class:`.InstrumentedAttribute` or user-defined
|
||||
attribute.
|
||||
|
||||
"""
|
||||
insp = inspection.inspect(entity)
|
||||
if insp.is_selectable:
|
||||
description = entity
|
||||
entity = insp.c
|
||||
elif insp.is_aliased_class:
|
||||
entity = insp.entity
|
||||
description = entity
|
||||
elif hasattr(insp, "mapper"):
|
||||
description = entity = insp.mapper.class_
|
||||
else:
|
||||
description = entity
|
||||
|
||||
try:
|
||||
return getattr(entity, key)
|
||||
except AttributeError:
|
||||
raise sa_exc.InvalidRequestError(
|
||||
"Entity '%s' has no property '%s'" %
|
||||
(description, key)
|
||||
)
|
||||
|
||||
_state_mapper = util.dottedgetter('manager.mapper')
|
||||
|
||||
@inspection._inspects(type)
|
||||
def _inspect_mapped_class(class_, configure=False):
|
||||
try:
|
||||
class_manager = manager_of_class(class_)
|
||||
if not class_manager.is_mapped:
|
||||
return None
|
||||
mapper = class_manager.mapper
|
||||
if configure and mapper._new_mappers:
|
||||
mapper._configure_all()
|
||||
return mapper
|
||||
|
||||
except exc.NO_STATE:
|
||||
return None
|
||||
|
||||
def class_mapper(class_, configure=True):
|
||||
"""Given a class, return the primary :class:`.Mapper` associated
|
||||
with the key.
|
||||
|
||||
Raises :exc:`.UnmappedClassError` if no mapping is configured
|
||||
on the given class, or :exc:`.ArgumentError` if a non-class
|
||||
object is passed.
|
||||
|
||||
Equivalent functionality is available via the :func:`.inspect`
|
||||
function as::
|
||||
|
||||
inspect(some_mapped_class)
|
||||
|
||||
Using the inspection system will raise
|
||||
:class:`sqlalchemy.exc.NoInspectionAvailable` if the class is not mapped.
|
||||
|
||||
"""
|
||||
mapper = _inspect_mapped_class(class_, configure=configure)
|
||||
if mapper is None:
|
||||
if not isinstance(class_, type):
|
||||
raise sa_exc.ArgumentError(
|
||||
"Class object expected, got '%r'." % class_)
|
||||
raise exc.UnmappedClassError(class_)
|
||||
else:
|
||||
return mapper
|
||||
|
||||
|
||||
class _InspectionAttr(object):
|
||||
"""A base class applied to all ORM objects that can be returned
|
||||
by the :func:`.inspect` function.
|
||||
|
||||
The attributes defined here allow the usage of simple boolean
|
||||
checks to test basic facts about the object returned.
|
||||
|
||||
While the boolean checks here are basically the same as using
|
||||
the Python isinstance() function, the flags here can be used without
|
||||
the need to import all of these classes, and also such that
|
||||
the SQLAlchemy class system can change while leaving the flags
|
||||
here intact for forwards-compatibility.
|
||||
|
||||
"""
|
||||
|
||||
is_selectable = False
|
||||
"""Return True if this object is an instance of :class:`.Selectable`."""
|
||||
|
||||
is_aliased_class = False
|
||||
"""True if this object is an instance of :class:`.AliasedClass`."""
|
||||
|
||||
is_instance = False
|
||||
"""True if this object is an instance of :class:`.InstanceState`."""
|
||||
|
||||
is_mapper = False
|
||||
"""True if this object is an instance of :class:`.Mapper`."""
|
||||
|
||||
is_property = False
|
||||
"""True if this object is an instance of :class:`.MapperProperty`."""
|
||||
|
||||
is_attribute = False
|
||||
"""True if this object is a Python :term:`descriptor`.
|
||||
|
||||
This can refer to one of many types. Usually a
|
||||
:class:`.QueryableAttribute` which handles attributes events on behalf
|
||||
of a :class:`.MapperProperty`. But can also be an extension type
|
||||
such as :class:`.AssociationProxy` or :class:`.hybrid_property`.
|
||||
The :attr:`._InspectionAttr.extension_type` will refer to a constant
|
||||
identifying the specific subtype.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:attr:`.Mapper.all_orm_descriptors`
|
||||
|
||||
"""
|
||||
|
||||
is_clause_element = False
|
||||
"""True if this object is an instance of :class:`.ClauseElement`."""
|
||||
|
||||
extension_type = NOT_EXTENSION
|
||||
"""The extension type, if any.
|
||||
Defaults to :data:`.interfaces.NOT_EXTENSION`
|
||||
|
||||
.. versionadded:: 0.8.0
|
||||
|
||||
.. seealso::
|
||||
|
||||
:data:`.HYBRID_METHOD`
|
||||
|
||||
:data:`.HYBRID_PROPERTY`
|
||||
|
||||
:data:`.ASSOCIATION_PROXY`
|
||||
|
||||
"""
|
||||
|
||||
class _MappedAttribute(object):
|
||||
"""Mixin for attributes which should be replaced by mapper-assigned
|
||||
attributes.
|
||||
|
||||
"""
|
||||
+284
-312
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
# orm/dependency.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 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,10 +8,11 @@
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy import sql, util, exc as sa_exc
|
||||
from sqlalchemy.orm import attributes, exc, sync, unitofwork, \
|
||||
from .. import sql, util, exc as sa_exc
|
||||
from . import attributes, exc, sync, unitofwork, \
|
||||
util as mapperutil
|
||||
from sqlalchemy.orm.interfaces import ONETOMANY, MANYTOONE, MANYTOMANY
|
||||
from .interfaces import ONETOMANY, MANYTOONE, MANYTOMANY
|
||||
|
||||
|
||||
class DependencyProcessor(object):
|
||||
def __init__(self, prop):
|
||||
@@ -32,7 +33,7 @@ class DependencyProcessor(object):
|
||||
if self.passive_updates:
|
||||
self._passive_update_flag = attributes.PASSIVE_NO_INITIALIZE
|
||||
else:
|
||||
self._passive_update_flag= attributes.PASSIVE_OFF
|
||||
self._passive_update_flag = attributes.PASSIVE_OFF
|
||||
|
||||
self.key = prop.key
|
||||
if not self.prop.synchronize_pairs:
|
||||
@@ -63,7 +64,6 @@ class DependencyProcessor(object):
|
||||
"""
|
||||
uow.register_preprocessor(self, True)
|
||||
|
||||
|
||||
def per_property_flush_actions(self, uow):
|
||||
after_save = unitofwork.ProcessAll(uow, self, False, True)
|
||||
before_delete = unitofwork.ProcessAll(uow, self, True, True)
|
||||
@@ -95,7 +95,6 @@ class DependencyProcessor(object):
|
||||
before_delete
|
||||
)
|
||||
|
||||
|
||||
def per_state_flush_actions(self, uow, states, isdelete):
|
||||
"""establish actions and dependencies related to a flush.
|
||||
|
||||
@@ -159,7 +158,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_ = state.manager[self.key].impl.get_all_pending(state, state.dict)
|
||||
sum_ = state.manager[self.key].impl.get_all_pending(
|
||||
state, state.dict)
|
||||
|
||||
if not sum_:
|
||||
continue
|
||||
@@ -210,7 +210,6 @@ class DependencyProcessor(object):
|
||||
after_save, before_delete,
|
||||
isdelete, childisdelete)
|
||||
|
||||
|
||||
def presort_deletes(self, uowcommit, states):
|
||||
return False
|
||||
|
||||
@@ -247,7 +246,11 @@ class DependencyProcessor(object):
|
||||
self.mapper in uowcommit.mappers
|
||||
|
||||
def _verify_canload(self, state):
|
||||
if state is not None and \
|
||||
if self.prop.uselist and state is None:
|
||||
raise exc.FlushError(
|
||||
"Can't flush None value found in "
|
||||
"collection %s" % (self.prop, ))
|
||||
elif state is not None and \
|
||||
not self.mapper._canload(state,
|
||||
allow_subtypes=not self.enable_typechecks):
|
||||
if self.mapper._canload(state, allow_subtypes=True):
|
||||
@@ -310,6 +313,7 @@ class DependencyProcessor(object):
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (self.__class__.__name__, self.prop)
|
||||
|
||||
|
||||
class OneToManyDP(DependencyProcessor):
|
||||
|
||||
def per_property_dependencies(self, uow, parent_saves,
|
||||
@@ -433,8 +437,6 @@ class OneToManyDP(DependencyProcessor):
|
||||
uowcommit.register_object(child,
|
||||
operation="delete", prop=self.prop)
|
||||
|
||||
|
||||
|
||||
def presort_saves(self, uowcommit, states):
|
||||
children_added = uowcommit.memo(('children_added', self), set)
|
||||
|
||||
@@ -559,10 +561,10 @@ class OneToManyDP(DependencyProcessor):
|
||||
pks_changed):
|
||||
source = state
|
||||
dest = child
|
||||
self._verify_canload(child)
|
||||
if dest is None or \
|
||||
(not self.post_update and uowcommit.is_deleted(dest)):
|
||||
return
|
||||
self._verify_canload(child)
|
||||
if clearkeys:
|
||||
sync.clear(dest, self.mapper, self.prop.synchronize_pairs)
|
||||
else:
|
||||
@@ -577,6 +579,7 @@ class OneToManyDP(DependencyProcessor):
|
||||
self.parent,
|
||||
self.prop.synchronize_pairs)
|
||||
|
||||
|
||||
class ManyToOneDP(DependencyProcessor):
|
||||
def __init__(self, prop):
|
||||
DependencyProcessor.__init__(self, prop)
|
||||
@@ -690,8 +693,8 @@ class ManyToOneDP(DependencyProcessor):
|
||||
continue
|
||||
uowcommit.register_object(child, isdelete=True,
|
||||
operation="delete", prop=self.prop)
|
||||
for c, m, st_, dct_ in self.mapper.cascade_iterator(
|
||||
'delete', child):
|
||||
t = self.mapper.cascade_iterator('delete', child)
|
||||
for c, m, st_, dct_ in t:
|
||||
uowcommit.register_object(
|
||||
st_, isdelete=True)
|
||||
|
||||
@@ -704,17 +707,14 @@ class ManyToOneDP(DependencyProcessor):
|
||||
self.key,
|
||||
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,
|
||||
operation="delete", prop=self.prop)
|
||||
|
||||
for c, m, st_, dct_ in self.mapper.cascade_iterator(
|
||||
'delete', child):
|
||||
uowcommit.register_object(
|
||||
st_,
|
||||
isdelete=True)
|
||||
t = self.mapper.cascade_iterator('delete', child)
|
||||
for c, m, st_, dct_ in t:
|
||||
uowcommit.register_object(st_, isdelete=True)
|
||||
|
||||
def process_deletes(self, uowcommit, states):
|
||||
if self.post_update and \
|
||||
@@ -773,6 +773,7 @@ class ManyToOneDP(DependencyProcessor):
|
||||
uowcommit,
|
||||
False)
|
||||
|
||||
|
||||
class DetectKeySwitch(DependencyProcessor):
|
||||
"""For many-to-one relationships with no one-to-many backref,
|
||||
searches for parents through the unit of work when a primary
|
||||
@@ -862,7 +863,7 @@ class DetectKeySwitch(DependencyProcessor):
|
||||
related = state.get_impl(self.key).get(state, dict_,
|
||||
passive=self._passive_update_flag)
|
||||
if related is not attributes.PASSIVE_NO_RESULT and \
|
||||
related is not None:
|
||||
related is not None:
|
||||
related_state = attributes.instance_state(dict_[self.key])
|
||||
if related_state in switchers:
|
||||
uowcommit.register_object(state,
|
||||
@@ -932,12 +933,14 @@ class ManyToManyDP(DependencyProcessor):
|
||||
])
|
||||
|
||||
def presort_deletes(self, uowcommit, states):
|
||||
# TODO: no tests fail if this whole
|
||||
# thing is removed !!!!
|
||||
if not self.passive_deletes:
|
||||
# if no passive deletes, load history on
|
||||
# the collection, so that prop_has_changes()
|
||||
# returns True
|
||||
for state in states:
|
||||
history = uowcommit.get_attribute_history(
|
||||
uowcommit.get_attribute_history(
|
||||
state,
|
||||
self.key,
|
||||
self._passive_delete_flag)
|
||||
@@ -1031,8 +1034,7 @@ class ManyToManyDP(DependencyProcessor):
|
||||
passive)
|
||||
if history:
|
||||
for child in history.added:
|
||||
if child is None or \
|
||||
(processed is not None and
|
||||
if (processed is not None and
|
||||
(state, child) in processed):
|
||||
continue
|
||||
associationrow = {}
|
||||
@@ -1043,8 +1045,7 @@ class ManyToManyDP(DependencyProcessor):
|
||||
continue
|
||||
secondary_insert.append(associationrow)
|
||||
for child in history.deleted:
|
||||
if child is None or \
|
||||
(processed is not None and
|
||||
if (processed is not None and
|
||||
(state, child) in processed):
|
||||
continue
|
||||
associationrow = {}
|
||||
@@ -1097,11 +1098,11 @@ class ManyToManyDP(DependencyProcessor):
|
||||
if result.supports_sane_multi_rowcount() and \
|
||||
result.rowcount != len(secondary_delete):
|
||||
raise exc.StaleDataError(
|
||||
"DELETE statement on table '%s' expected to delete %d row(s); "
|
||||
"Only %d were matched." %
|
||||
(self.secondary.description, len(secondary_delete),
|
||||
result.rowcount)
|
||||
)
|
||||
"DELETE statement on table '%s' expected to delete "
|
||||
"%d row(s); Only %d were matched." %
|
||||
(self.secondary.description, len(secondary_delete),
|
||||
result.rowcount)
|
||||
)
|
||||
|
||||
if secondary_update:
|
||||
associationrow = secondary_update[0]
|
||||
@@ -1114,11 +1115,11 @@ class ManyToManyDP(DependencyProcessor):
|
||||
if result.supports_sane_multi_rowcount() and \
|
||||
result.rowcount != len(secondary_update):
|
||||
raise exc.StaleDataError(
|
||||
"UPDATE statement on table '%s' expected to update %d row(s); "
|
||||
"Only %d were matched." %
|
||||
(self.secondary.description, len(secondary_update),
|
||||
result.rowcount)
|
||||
)
|
||||
"UPDATE statement on table '%s' expected to update "
|
||||
"%d row(s); Only %d were matched." %
|
||||
(self.secondary.description, len(secondary_update),
|
||||
result.rowcount)
|
||||
)
|
||||
|
||||
if secondary_insert:
|
||||
statement = self.secondary.insert()
|
||||
@@ -1126,8 +1127,14 @@ class ManyToManyDP(DependencyProcessor):
|
||||
|
||||
def _synchronize(self, state, child, associationrow,
|
||||
clearkeys, uowcommit, operation):
|
||||
if associationrow is None:
|
||||
return
|
||||
|
||||
# this checks for None if uselist=True
|
||||
self._verify_canload(child)
|
||||
|
||||
# but if uselist=False we get here. If child is None,
|
||||
# no association row can be generated, so return.
|
||||
if child is None:
|
||||
return False
|
||||
|
||||
if child is not None and not uowcommit.session._contains_state(child):
|
||||
if not child.deleted:
|
||||
@@ -1137,8 +1144,6 @@ class ManyToManyDP(DependencyProcessor):
|
||||
(mapperutil.state_class_str(child), operation, self.prop))
|
||||
return False
|
||||
|
||||
self._verify_canload(child)
|
||||
|
||||
sync.populate_dict(state, self.parent, associationrow,
|
||||
self.prop.synchronize_pairs)
|
||||
sync.populate_dict(child, self.mapper, associationrow,
|
||||
@@ -1154,8 +1159,7 @@ class ManyToManyDP(DependencyProcessor):
|
||||
self.prop.synchronize_pairs)
|
||||
|
||||
_direction_to_processor = {
|
||||
ONETOMANY : OneToManyDP,
|
||||
ONETOMANY: OneToManyDP,
|
||||
MANYTOONE: ManyToOneDP,
|
||||
MANYTOMANY : ManyToManyDP,
|
||||
MANYTOMANY: ManyToManyDP,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# orm/deprecated_interfaces.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 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
|
||||
|
||||
from .. import event, util
|
||||
from .interfaces import EXT_CONTINUE
|
||||
|
||||
@util.langhelpers.dependency_for("sqlalchemy.orm.interfaces")
|
||||
class MapperExtension(object):
|
||||
"""Base implementation for :class:`.Mapper` event hooks.
|
||||
|
||||
@@ -116,7 +116,6 @@ class MapperExtension(object):
|
||||
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.
|
||||
@@ -374,6 +373,8 @@ class MapperExtension(object):
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
|
||||
@util.langhelpers.dependency_for("sqlalchemy.orm.interfaces")
|
||||
class SessionExtension(object):
|
||||
|
||||
"""Base implementation for :class:`.Session` event hooks.
|
||||
@@ -385,7 +386,7 @@ class SessionExtension(object):
|
||||
:class:`.SessionEvents`.
|
||||
|
||||
Subclasses may be installed into a :class:`.Session` (or
|
||||
:func:`.sessionmaker`) using the ``extension`` keyword
|
||||
:class:`.sessionmaker`) using the ``extension`` keyword
|
||||
argument::
|
||||
|
||||
from sqlalchemy.orm.interfaces import SessionExtension
|
||||
@@ -439,7 +440,7 @@ class SessionExtension(object):
|
||||
Note that this may not be per-flush if a longer running
|
||||
transaction is ongoing."""
|
||||
|
||||
def before_flush( self, session, flush_context, instances):
|
||||
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
|
||||
@@ -462,7 +463,7 @@ class SessionExtension(object):
|
||||
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):
|
||||
def after_begin(self, session, transaction, connection):
|
||||
"""Execute after a transaction is begun on a connection
|
||||
|
||||
`transaction` is the SessionTransaction. This method is called
|
||||
@@ -473,7 +474,7 @@ class SessionExtension(object):
|
||||
|
||||
This is called after an add, delete or merge. """
|
||||
|
||||
def after_bulk_update( self, session, query, query_context, result):
|
||||
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()
|
||||
@@ -483,7 +484,7 @@ class SessionExtension(object):
|
||||
`result` is the result object returned from the bulk operation.
|
||||
"""
|
||||
|
||||
def after_bulk_delete( self, session, query, query_context, result):
|
||||
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()
|
||||
@@ -494,6 +495,7 @@ class SessionExtension(object):
|
||||
"""
|
||||
|
||||
|
||||
@util.langhelpers.dependency_for("sqlalchemy.orm.interfaces")
|
||||
class AttributeExtension(object):
|
||||
"""Base implementation for :class:`.AttributeImpl` event hooks, events
|
||||
that fire upon attribute mutations in user code.
|
||||
@@ -586,5 +588,3 @@ class AttributeExtension(object):
|
||||
|
||||
"""
|
||||
return value
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# orm/descriptor_props.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 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
|
||||
@@ -10,13 +10,14 @@ 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')
|
||||
from .interfaces import MapperProperty, PropComparator
|
||||
from .util import _none_set
|
||||
from . import attributes
|
||||
from .. import util, sql, exc as sa_exc, event, schema
|
||||
from ..sql import expression
|
||||
from . import properties
|
||||
from . import query
|
||||
|
||||
|
||||
class DescriptorProperty(MapperProperty):
|
||||
""":class:`.MapperProperty` which proxies access to a
|
||||
@@ -30,6 +31,7 @@ class DescriptorProperty(MapperProperty):
|
||||
class _ProxyImpl(object):
|
||||
accepts_scalar_loader = False
|
||||
expire_missing = True
|
||||
collection = False
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
@@ -47,8 +49,10 @@ class DescriptorProperty(MapperProperty):
|
||||
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)
|
||||
|
||||
@@ -65,15 +69,79 @@ class DescriptorProperty(MapperProperty):
|
||||
self.key,
|
||||
self.descriptor,
|
||||
lambda: self._comparator_factory(mapper),
|
||||
doc=self.doc
|
||||
doc=self.doc,
|
||||
original_property=self
|
||||
)
|
||||
proxy_attr.impl = _ProxyImpl(self.key)
|
||||
mapper.class_manager.instrument_attribute(self.key, proxy_attr)
|
||||
|
||||
|
||||
@util.langhelpers.dependency_for("sqlalchemy.orm.properties")
|
||||
class CompositeProperty(DescriptorProperty):
|
||||
"""Defines a "composite" mapped attribute, representing a collection
|
||||
of columns as one attribute.
|
||||
|
||||
:class:`.CompositeProperty` is constructed using the :func:`.composite`
|
||||
function.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`mapper_composite`
|
||||
|
||||
"""
|
||||
def __init__(self, class_, *attrs, **kwargs):
|
||||
"""Return a composite column-based property for use with a Mapper.
|
||||
|
||||
See the mapping documentation section :ref:`mapper_composite` for a full
|
||||
usage example.
|
||||
|
||||
The :class:`.MapperProperty` returned by :func:`.composite`
|
||||
is the :class:`.CompositeProperty`.
|
||||
|
||||
:param class\_:
|
||||
The "composite type" class.
|
||||
|
||||
:param \*cols:
|
||||
List of Column objects to be mapped.
|
||||
|
||||
:param active_history=False:
|
||||
When ``True``, indicates that the "previous" value for a
|
||||
scalar attribute should be loaded when replaced, if not
|
||||
already loaded. See the same flag on :func:`.column_property`.
|
||||
|
||||
.. versionchanged:: 0.7
|
||||
This flag specifically becomes meaningful
|
||||
- previously it was a placeholder.
|
||||
|
||||
:param group:
|
||||
A group name for this property when marked as deferred.
|
||||
|
||||
:param deferred:
|
||||
When True, the column property is "deferred", meaning that it does not
|
||||
load immediately, and is instead loaded when the attribute is first
|
||||
accessed on an instance. See also :func:`~sqlalchemy.orm.deferred`.
|
||||
|
||||
:param comparator_factory: a class which extends
|
||||
:class:`.CompositeProperty.Comparator` which provides custom SQL clause
|
||||
generation for comparison operations.
|
||||
|
||||
:param doc:
|
||||
optional string that will be applied as the doc on the
|
||||
class-bound descriptor.
|
||||
|
||||
:param info: Optional data dictionary which will be populated into the
|
||||
:attr:`.MapperProperty.info` attribute of this object.
|
||||
|
||||
.. versionadded:: 0.8
|
||||
|
||||
:param extension:
|
||||
an :class:`.AttributeExtension` instance,
|
||||
or list of extensions, which will be prepended to the list of
|
||||
attribute listeners for the resulting descriptor placed on the class.
|
||||
**Deprecated.** Please see :class:`.AttributeEvents`.
|
||||
|
||||
"""
|
||||
|
||||
self.attrs = attrs
|
||||
self.composite_class = class_
|
||||
self.active_history = kwargs.get('active_history', False)
|
||||
@@ -81,9 +149,13 @@ class CompositeProperty(DescriptorProperty):
|
||||
self.group = kwargs.get('group', None)
|
||||
self.comparator_factory = kwargs.pop('comparator_factory',
|
||||
self.__class__.Comparator)
|
||||
if 'info' in kwargs:
|
||||
self.info = kwargs.pop('info')
|
||||
|
||||
util.set_creation_order(self)
|
||||
self._create_descriptor()
|
||||
|
||||
|
||||
def instrument_class(self, mapper):
|
||||
super(CompositeProperty, self).instrument_class(mapper)
|
||||
self._setup_event_handlers()
|
||||
@@ -110,7 +182,10 @@ class CompositeProperty(DescriptorProperty):
|
||||
# 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]
|
||||
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
|
||||
@@ -164,12 +239,17 @@ class CompositeProperty(DescriptorProperty):
|
||||
def _init_props(self):
|
||||
self.props = props = []
|
||||
for attr in self.attrs:
|
||||
if isinstance(attr, basestring):
|
||||
if isinstance(attr, str):
|
||||
prop = self.parent.get_property(attr)
|
||||
elif isinstance(attr, schema.Column):
|
||||
prop = self.parent._columntoproperty[attr]
|
||||
elif isinstance(attr, attributes.InstrumentedAttribute):
|
||||
prop = attr.property
|
||||
else:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Composite expects Column objects or mapped "
|
||||
"attributes/attribute names as arguments, got: %r"
|
||||
% (attr,))
|
||||
props.append(prop)
|
||||
|
||||
@property
|
||||
@@ -185,7 +265,9 @@ class CompositeProperty(DescriptorProperty):
|
||||
prop.active_history = self.active_history
|
||||
if self.deferred:
|
||||
prop.deferred = self.deferred
|
||||
prop.strategy_class = strategies.DeferredColumnLoader
|
||||
prop.strategy_class = prop._strategy_lookup(
|
||||
("deferred", True),
|
||||
("instrument", True))
|
||||
prop.group = self.group
|
||||
|
||||
def _setup_event_handlers(self):
|
||||
@@ -225,12 +307,15 @@ class CompositeProperty(DescriptorProperty):
|
||||
state.dict.pop(self.key, None)
|
||||
|
||||
event.listen(self.parent, 'after_insert',
|
||||
insert_update_handler, raw=True)
|
||||
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)
|
||||
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
|
||||
|
||||
@@ -271,34 +356,80 @@ class CompositeProperty(DescriptorProperty):
|
||||
)
|
||||
else:
|
||||
return attributes.History(
|
||||
(),[self.composite_class(*added)], ()
|
||||
(), [self.composite_class(*added)], ()
|
||||
)
|
||||
|
||||
def _comparator_factory(self, mapper):
|
||||
return self.comparator_factory(self)
|
||||
return self.comparator_factory(self, mapper)
|
||||
|
||||
class CompositeBundle(query.Bundle):
|
||||
def __init__(self, property, expr):
|
||||
self.property = property
|
||||
super(CompositeProperty.CompositeBundle, self).__init__(
|
||||
property.key, *expr)
|
||||
|
||||
def create_row_processor(self, query, procs, labels):
|
||||
def proc(row, result):
|
||||
return self.property.composite_class(*[proc(row, result) for proc in procs])
|
||||
return proc
|
||||
|
||||
|
||||
class Comparator(PropComparator):
|
||||
def __init__(self, prop, adapter=None):
|
||||
self.prop = self.property = prop
|
||||
self.adapter = adapter
|
||||
"""Produce boolean, comparison, and other operators for
|
||||
:class:`.CompositeProperty` attributes.
|
||||
|
||||
See the example in :ref:`composite_operations` for an overview
|
||||
of usage , as well as the documentation for :class:`.PropComparator`.
|
||||
|
||||
See also:
|
||||
|
||||
:class:`.PropComparator`
|
||||
|
||||
:class:`.ColumnOperators`
|
||||
|
||||
:ref:`types_operators`
|
||||
|
||||
:attr:`.TypeEngine.comparator_factory`
|
||||
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
def clauses(self):
|
||||
return self.__clause_element__()
|
||||
|
||||
def __clause_element__(self):
|
||||
return expression.ClauseList(group=False, *self._comparable_elements)
|
||||
|
||||
def _query_clause_element(self):
|
||||
return CompositeProperty.CompositeBundle(self.prop, self.__clause_element__())
|
||||
|
||||
@util.memoized_property
|
||||
def _comparable_elements(self):
|
||||
if self._adapt_to_entity:
|
||||
return [
|
||||
getattr(
|
||||
self._adapt_to_entity.entity,
|
||||
prop.key
|
||||
) for prop in self.prop._comparable_elements
|
||||
]
|
||||
else:
|
||||
return self.prop._comparable_elements
|
||||
|
||||
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)])
|
||||
comparisons = [
|
||||
a == b
|
||||
for a, b in zip(self.prop._comparable_elements, values)
|
||||
]
|
||||
if self._adapt_to_entity:
|
||||
comparisons = [self.adapter(x) for x in comparisons]
|
||||
return sql.and_(*comparisons)
|
||||
|
||||
def __ne__(self, other):
|
||||
return sql.not_(self.__eq__(other))
|
||||
@@ -306,6 +437,8 @@ class CompositeProperty(DescriptorProperty):
|
||||
def __str__(self):
|
||||
return str(self.parent.class_.__name__) + "." + self.key
|
||||
|
||||
|
||||
@util.langhelpers.dependency_for("sqlalchemy.orm.properties")
|
||||
class ConcreteInheritedProperty(DescriptorProperty):
|
||||
"""A 'do nothing' :class:`.MapperProperty` that disables
|
||||
an attribute on a concrete subclass that is only present
|
||||
@@ -343,8 +476,10 @@ class ConcreteInheritedProperty(DescriptorProperty):
|
||||
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
|
||||
@@ -352,11 +487,66 @@ class ConcreteInheritedProperty(DescriptorProperty):
|
||||
self.descriptor = NoninheritedConcreteProp()
|
||||
|
||||
|
||||
@util.langhelpers.dependency_for("sqlalchemy.orm.properties")
|
||||
class SynonymProperty(DescriptorProperty):
|
||||
|
||||
def __init__(self, name, map_column=None,
|
||||
descriptor=None, comparator_factory=None,
|
||||
doc=None):
|
||||
"""Denote an attribute name as a synonym to a mapped property,
|
||||
in that the attribute will mirror the value and expression behavior
|
||||
of another attribute.
|
||||
|
||||
:param name: the name of the existing mapped property. This
|
||||
can refer to the string name of any :class:`.MapperProperty`
|
||||
configured on the class, including column-bound attributes
|
||||
and relationships.
|
||||
|
||||
:param descriptor: a Python :term:`descriptor` that will be used
|
||||
as a getter (and potentially a setter) when this attribute is
|
||||
accessed at the instance level.
|
||||
|
||||
:param map_column: if ``True``, the :func:`.synonym` construct will
|
||||
locate the existing named :class:`.MapperProperty` based on the
|
||||
attribute name of this :func:`.synonym`, and assign it to a new
|
||||
attribute linked to the name of this :func:`.synonym`.
|
||||
That is, given a mapping like::
|
||||
|
||||
class MyClass(Base):
|
||||
__tablename__ = 'my_table'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
job_status = Column(String(50))
|
||||
|
||||
job_status = synonym("_job_status", map_column=True)
|
||||
|
||||
The above class ``MyClass`` will now have the ``job_status``
|
||||
:class:`.Column` object mapped to the attribute named ``_job_status``,
|
||||
and the attribute named ``job_status`` will refer to the synonym
|
||||
itself. This feature is typically used in conjunction with the
|
||||
``descriptor`` argument in order to link a user-defined descriptor
|
||||
as a "wrapper" for an existing column.
|
||||
|
||||
:param comparator_factory: A subclass of :class:`.PropComparator`
|
||||
that will provide custom comparison behavior at the SQL expression
|
||||
level.
|
||||
|
||||
.. note::
|
||||
|
||||
For the use case of providing an attribute which redefines both
|
||||
Python-level and SQL-expression level behavior of an attribute,
|
||||
please refer to the Hybrid attribute introduced at
|
||||
:ref:`mapper_hybrids` for a more effective technique.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`synonyms` - examples of functionality.
|
||||
|
||||
:ref:`mapper_hybrids` - Hybrids provide a better approach for
|
||||
more complicated attribute-wrapping schemes than synonyms.
|
||||
|
||||
"""
|
||||
|
||||
self.name = name
|
||||
self.map_column = map_column
|
||||
self.descriptor = descriptor
|
||||
@@ -409,10 +599,73 @@ class SynonymProperty(DescriptorProperty):
|
||||
|
||||
self.parent = parent
|
||||
|
||||
|
||||
@util.langhelpers.dependency_for("sqlalchemy.orm.properties")
|
||||
class ComparableProperty(DescriptorProperty):
|
||||
"""Instruments a Python property for use in query expressions."""
|
||||
|
||||
def __init__(self, comparator_factory, descriptor=None, doc=None):
|
||||
"""Provides a method of applying a :class:`.PropComparator`
|
||||
to any Python descriptor attribute.
|
||||
|
||||
.. versionchanged:: 0.7
|
||||
:func:`.comparable_property` is superseded by
|
||||
the :mod:`~sqlalchemy.ext.hybrid` extension. See the example
|
||||
at :ref:`hybrid_custom_comparators`.
|
||||
|
||||
Allows any Python descriptor to behave like a SQL-enabled
|
||||
attribute when used at the class level in queries, allowing
|
||||
redefinition of expression operator behavior.
|
||||
|
||||
In the example below we redefine :meth:`.PropComparator.operate`
|
||||
to wrap both sides of an expression in ``func.lower()`` to produce
|
||||
case-insensitive comparison::
|
||||
|
||||
from sqlalchemy.orm import comparable_property
|
||||
from sqlalchemy.orm.interfaces import PropComparator
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy import Integer, String, Column
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
class CaseInsensitiveComparator(PropComparator):
|
||||
def __clause_element__(self):
|
||||
return self.prop
|
||||
|
||||
def operate(self, op, other):
|
||||
return op(
|
||||
func.lower(self.__clause_element__()),
|
||||
func.lower(other)
|
||||
)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
class SearchWord(Base):
|
||||
__tablename__ = 'search_word'
|
||||
id = Column(Integer, primary_key=True)
|
||||
word = Column(String)
|
||||
word_insensitive = comparable_property(lambda prop, mapper:
|
||||
CaseInsensitiveComparator(mapper.c.word, mapper)
|
||||
)
|
||||
|
||||
|
||||
A mapping like the above allows the ``word_insensitive`` attribute
|
||||
to render an expression like::
|
||||
|
||||
>>> print SearchWord.word_insensitive == "Trucks"
|
||||
lower(search_word.word) = lower(:lower_1)
|
||||
|
||||
:param comparator_factory:
|
||||
A PropComparator subclass or factory that defines operator behavior
|
||||
for this property.
|
||||
|
||||
:param descriptor:
|
||||
Optional when used in a ``properties={}`` declaration. The Python
|
||||
descriptor or property to layer comparison behavior on top of.
|
||||
|
||||
The like-named descriptor will be automatically retrieved from the
|
||||
mapped class if left blank in a ``properties`` declaration.
|
||||
|
||||
"""
|
||||
self.descriptor = descriptor
|
||||
self.comparator_factory = comparator_factory
|
||||
self.doc = doc or (descriptor and descriptor.__doc__) or None
|
||||
@@ -420,3 +673,5 @@ class ComparableProperty(DescriptorProperty):
|
||||
|
||||
def _comparator_factory(self, mapper):
|
||||
return self.comparator_factory(self, mapper)
|
||||
|
||||
|
||||
|
||||
+130
-82
@@ -1,5 +1,5 @@
|
||||
# orm/dynamic.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 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
|
||||
@@ -11,39 +11,40 @@ basic add/delete mutation.
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy import log, util
|
||||
from sqlalchemy.orm import exc as orm_exc
|
||||
from sqlalchemy.sql import operators
|
||||
from sqlalchemy.orm import (
|
||||
attributes, object_session, util as mapperutil, strategies, object_mapper
|
||||
from .. import log, util, exc
|
||||
from ..sql import operators
|
||||
from . import (
|
||||
attributes, object_session, util as orm_util, strategies,
|
||||
object_mapper, exc as orm_exc, properties
|
||||
)
|
||||
from sqlalchemy.orm.query import Query
|
||||
from sqlalchemy.orm.util import has_identity
|
||||
from sqlalchemy.orm import collections
|
||||
from .query import Query
|
||||
|
||||
@log.class_logger
|
||||
@properties.RelationshipProperty.strategy_for(lazy="dynamic")
|
||||
class DynaLoader(strategies.AbstractRelationshipLoader):
|
||||
def init_class_attribute(self, mapper):
|
||||
self.is_class_level = True
|
||||
if not self.uselist:
|
||||
util.warn(
|
||||
"On relationship %s, 'dynamic' loaders cannot be used with "
|
||||
"many-to-one/one-to-one relationships and/or "
|
||||
"uselist=False." % self.parent_property)
|
||||
raise exc.InvalidRequestError(
|
||||
"On relationship %s, 'dynamic' loaders cannot be used with "
|
||||
"many-to-one/one-to-one relationships and/or "
|
||||
"uselist=False." % self.parent_property)
|
||||
strategies._register_attribute(self,
|
||||
mapper,
|
||||
useobject=True,
|
||||
uselist=True,
|
||||
impl_class=DynamicAttributeImpl,
|
||||
target_mapper=self.parent_property.mapper,
|
||||
order_by=self.parent_property.order_by,
|
||||
query_class=self.parent_property.query_class
|
||||
query_class=self.parent_property.query_class,
|
||||
backref=self.parent_property.back_populates,
|
||||
)
|
||||
|
||||
log.class_logger(DynaLoader)
|
||||
|
||||
class DynamicAttributeImpl(attributes.AttributeImpl):
|
||||
uses_objects = True
|
||||
accepts_scalar_loader = False
|
||||
supports_population = False
|
||||
collection = False
|
||||
|
||||
def __init__(self, class_, key, typecallable,
|
||||
dispatch,
|
||||
@@ -60,7 +61,7 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
|
||||
self.query_class = mixin_user_query(query_class)
|
||||
|
||||
def get(self, state, dict_, passive=attributes.PASSIVE_OFF):
|
||||
if passive is not attributes.PASSIVE_OFF:
|
||||
if not passive & attributes.SQL_OK:
|
||||
return self._get_collection_history(state,
|
||||
attributes.PASSIVE_NO_INITIALIZE).added_items
|
||||
else:
|
||||
@@ -68,39 +69,53 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
|
||||
|
||||
def get_collection(self, state, dict_, user_data=None,
|
||||
passive=attributes.PASSIVE_NO_INITIALIZE):
|
||||
if passive is not attributes.PASSIVE_OFF:
|
||||
if not passive & attributes.SQL_OK:
|
||||
return self._get_collection_history(state,
|
||||
passive).added_items
|
||||
else:
|
||||
history = self._get_collection_history(state, passive)
|
||||
return history.added_items + history.unchanged_items
|
||||
return history.added_plus_unchanged
|
||||
|
||||
def fire_append_event(self, state, dict_, value, initiator):
|
||||
collection_history = self._modified_event(state, dict_)
|
||||
collection_history.added_items.append(value)
|
||||
@util.memoized_property
|
||||
def _append_token(self):
|
||||
return attributes.Event(self, attributes.OP_APPEND)
|
||||
|
||||
@util.memoized_property
|
||||
def _remove_token(self):
|
||||
return attributes.Event(self, attributes.OP_REMOVE)
|
||||
|
||||
def fire_append_event(self, state, dict_, value, initiator,
|
||||
collection_history=None):
|
||||
if collection_history is None:
|
||||
collection_history = self._modified_event(state, dict_)
|
||||
|
||||
collection_history.add_added(value)
|
||||
|
||||
for fn in self.dispatch.append:
|
||||
value = fn(state, value, initiator or self)
|
||||
value = fn(state, value, initiator or self._append_token)
|
||||
|
||||
if self.trackparent and value is not None:
|
||||
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)
|
||||
def fire_remove_event(self, state, dict_, value, initiator,
|
||||
collection_history=None):
|
||||
if collection_history is None:
|
||||
collection_history = self._modified_event(state, dict_)
|
||||
|
||||
collection_history.add_removed(value)
|
||||
|
||||
if self.trackparent and value is not None:
|
||||
self.sethasparent(attributes.instance_state(value), state, False)
|
||||
|
||||
for fn in self.dispatch.remove:
|
||||
fn(state, value, initiator or self)
|
||||
fn(state, value, initiator or self._remove_token)
|
||||
|
||||
def _modified_event(self, state, dict_):
|
||||
|
||||
if self.key not in state.committed_state:
|
||||
state.committed_state[self.key] = CollectionHistory(self, state)
|
||||
|
||||
state.modified_event(dict_,
|
||||
state._modified_event(dict_,
|
||||
self,
|
||||
attributes.NEVER_SET)
|
||||
|
||||
@@ -119,18 +134,31 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
|
||||
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)
|
||||
if state.has_identity:
|
||||
old_collection = list(self.get(state, dict_))
|
||||
old_collection = util.IdentitySet(self.get(state, dict_))
|
||||
|
||||
collection_history = self._modified_event(state, dict_)
|
||||
if not state.has_identity:
|
||||
old_collection = collection_history.added_items
|
||||
else:
|
||||
old_collection = []
|
||||
collections.bulk_replace(new_values, DynCollectionAdapter(self,
|
||||
state, old_collection),
|
||||
DynCollectionAdapter(self, state,
|
||||
new_values))
|
||||
old_collection = old_collection.union(
|
||||
collection_history.added_items)
|
||||
|
||||
idset = util.IdentitySet
|
||||
constants = old_collection.intersection(new_values)
|
||||
additions = idset(new_values).difference(constants)
|
||||
removals = old_collection.difference(constants)
|
||||
|
||||
for member in new_values:
|
||||
if member in additions:
|
||||
self.fire_append_event(state, dict_, member, None,
|
||||
collection_history=collection_history)
|
||||
|
||||
for member in removals:
|
||||
self.fire_remove_event(state, dict_, member, None,
|
||||
collection_history=collection_history)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
raise NotImplementedError()
|
||||
@@ -141,15 +169,15 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
|
||||
|
||||
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)
|
||||
return c.as_history()
|
||||
|
||||
def get_all_pending(self, state, dict_):
|
||||
c = self._get_collection_history(state, True)
|
||||
c = self._get_collection_history(
|
||||
state, attributes.PASSIVE_NO_INITIALIZE)
|
||||
return [
|
||||
(attributes.instance_state(x), x)
|
||||
for x in
|
||||
c.added_items + c.unchanged_items + c.deleted_items
|
||||
c.all_items
|
||||
]
|
||||
|
||||
def _get_collection_history(self, state, passive=attributes.PASSIVE_OFF):
|
||||
@@ -158,7 +186,7 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
|
||||
else:
|
||||
c = CollectionHistory(self, state)
|
||||
|
||||
if passive is attributes.PASSIVE_OFF:
|
||||
if state.has_identity and (passive & attributes.INIT_OK):
|
||||
return CollectionHistory(self, state, apply_to=c)
|
||||
else:
|
||||
return c
|
||||
@@ -173,34 +201,16 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
|
||||
if initiator is not self:
|
||||
self.fire_remove_event(state, dict_, value, initiator)
|
||||
|
||||
class DynCollectionAdapter(object):
|
||||
"""the dynamic analogue to orm.collections.CollectionAdapter"""
|
||||
def pop(self, state, dict_, value, initiator,
|
||||
passive=attributes.PASSIVE_OFF):
|
||||
self.remove(state, dict_, value, initiator, passive=passive)
|
||||
|
||||
def __init__(self, attr, owner_state, data):
|
||||
self.attr = attr
|
||||
self.state = owner_state
|
||||
self.data = data
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.data)
|
||||
|
||||
def append_with_event(self, item, initiator=None):
|
||||
self.attr.append(self.state, self.state.dict, item, initiator)
|
||||
|
||||
def remove_with_event(self, item, initiator=None):
|
||||
self.attr.remove(self.state, self.state.dict, item, initiator)
|
||||
|
||||
def append_without_event(self, item):
|
||||
pass
|
||||
|
||||
def remove_without_event(self, item):
|
||||
pass
|
||||
|
||||
class AppenderMixin(object):
|
||||
query_class = None
|
||||
|
||||
def __init__(self, attr, state):
|
||||
Query.__init__(self, attr.target_mapper, None)
|
||||
super(AppenderMixin, self).__init__(attr.target_mapper, None)
|
||||
self.instance = instance = state.obj()
|
||||
self.attr = attr
|
||||
|
||||
@@ -215,22 +225,19 @@ class AppenderMixin(object):
|
||||
if self.attr.order_by:
|
||||
self._order_by = self.attr.order_by
|
||||
|
||||
def __session(self):
|
||||
def session(self):
|
||||
sess = object_session(self.instance)
|
||||
if sess is not None and self.autoflush and sess.autoflush \
|
||||
and self.instance in sess:
|
||||
sess.flush()
|
||||
if not has_identity(self.instance):
|
||||
if not orm_util.has_identity(self.instance):
|
||||
return None
|
||||
else:
|
||||
return sess
|
||||
|
||||
def session(self):
|
||||
return self.__session()
|
||||
session = property(session, lambda s, x:None)
|
||||
session = property(session, lambda s, x: None)
|
||||
|
||||
def __iter__(self):
|
||||
sess = self.__session()
|
||||
sess = self.session
|
||||
if sess is None:
|
||||
return iter(self.attr._get_collection_history(
|
||||
attributes.instance_state(self.instance),
|
||||
@@ -239,17 +246,16 @@ class AppenderMixin(object):
|
||||
return iter(self._clone(sess))
|
||||
|
||||
def __getitem__(self, index):
|
||||
sess = self.__session()
|
||||
sess = self.session
|
||||
if sess is None:
|
||||
return self.attr._get_collection_history(
|
||||
attributes.instance_state(self.instance),
|
||||
attributes.PASSIVE_NO_INITIALIZE).added_items.\
|
||||
__getitem__(index)
|
||||
attributes.PASSIVE_NO_INITIALIZE).indexed(index)
|
||||
else:
|
||||
return self._clone(sess).__getitem__(index)
|
||||
|
||||
def count(self):
|
||||
sess = self.__session()
|
||||
sess = self.session
|
||||
if sess is None:
|
||||
return len(self.attr._get_collection_history(
|
||||
attributes.instance_state(self.instance),
|
||||
@@ -269,7 +275,7 @@ class AppenderMixin(object):
|
||||
"Parent instance %s is not bound to a Session, and no "
|
||||
"contextual session is established; lazy load operation "
|
||||
"of attribute '%s' cannot proceed" % (
|
||||
mapperutil.instance_str(instance), self.attr.key))
|
||||
orm_util.instance_str(instance), self.attr.key))
|
||||
|
||||
if self.query_class:
|
||||
query = self.query_class(self.attr.target_mapper, session=sess)
|
||||
@@ -281,6 +287,12 @@ class AppenderMixin(object):
|
||||
|
||||
return query
|
||||
|
||||
def extend(self, iterator):
|
||||
for item in iterator:
|
||||
self.attr.append(
|
||||
attributes.instance_state(self.instance),
|
||||
attributes.instance_dict(self.instance), item, None)
|
||||
|
||||
def append(self, item):
|
||||
self.attr.append(
|
||||
attributes.instance_state(self.instance),
|
||||
@@ -301,20 +313,56 @@ def mixin_user_query(cls):
|
||||
name = 'Appender' + cls.__name__
|
||||
return type(name, (AppenderMixin, cls), {'query_class': cls})
|
||||
|
||||
|
||||
class CollectionHistory(object):
|
||||
"""Overrides AttributeHistory to receive append/remove events directly."""
|
||||
|
||||
def __init__(self, attr, state, apply_to=None):
|
||||
if apply_to:
|
||||
deleted = util.IdentitySet(apply_to.deleted_items)
|
||||
added = apply_to.added_items
|
||||
coll = AppenderQuery(attr, state).autoflush(False)
|
||||
self.unchanged_items = [o for o in util.IdentitySet(coll)
|
||||
if o not in deleted]
|
||||
self.unchanged_items = util.OrderedIdentitySet(coll)
|
||||
self.added_items = apply_to.added_items
|
||||
self.deleted_items = apply_to.deleted_items
|
||||
self._reconcile_collection = True
|
||||
else:
|
||||
self.deleted_items = []
|
||||
self.added_items = []
|
||||
self.unchanged_items = []
|
||||
self.deleted_items = util.OrderedIdentitySet()
|
||||
self.added_items = util.OrderedIdentitySet()
|
||||
self.unchanged_items = util.OrderedIdentitySet()
|
||||
self._reconcile_collection = False
|
||||
|
||||
@property
|
||||
def added_plus_unchanged(self):
|
||||
return list(self.added_items.union(self.unchanged_items))
|
||||
|
||||
@property
|
||||
def all_items(self):
|
||||
return list(self.added_items.union(
|
||||
self.unchanged_items).union(self.deleted_items))
|
||||
|
||||
def as_history(self):
|
||||
if self._reconcile_collection:
|
||||
added = self.added_items.difference(self.unchanged_items)
|
||||
deleted = self.deleted_items.intersection(self.unchanged_items)
|
||||
unchanged = self.unchanged_items.difference(deleted)
|
||||
else:
|
||||
added, unchanged, deleted = self.added_items,\
|
||||
self.unchanged_items,\
|
||||
self.deleted_items
|
||||
return attributes.History(
|
||||
list(added),
|
||||
list(unchanged),
|
||||
list(deleted),
|
||||
)
|
||||
|
||||
def indexed(self, index):
|
||||
return list(self.added_items)[index]
|
||||
|
||||
def add_added(self, value):
|
||||
self.added_items.add(value)
|
||||
|
||||
def add_removed(self, value):
|
||||
if value in self.added_items:
|
||||
self.added_items.remove(value)
|
||||
else:
|
||||
self.deleted_items.add(value)
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
# orm/evaluator.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 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 operator
|
||||
from sqlalchemy.sql import operators, functions
|
||||
from sqlalchemy.sql import expression as sql
|
||||
from ..sql import operators
|
||||
|
||||
|
||||
class UnevaluatableError(Exception):
|
||||
@@ -14,23 +13,23 @@ class UnevaluatableError(Exception):
|
||||
|
||||
_straight_ops = set(getattr(operators, op)
|
||||
for op in ('add', 'mul', 'sub',
|
||||
# Py2K
|
||||
'div',
|
||||
# end Py2K
|
||||
'mod', 'truediv',
|
||||
'lt', 'le', 'ne', 'gt', 'ge', 'eq'))
|
||||
|
||||
|
||||
_notimplemented_ops = set(getattr(operators, op)
|
||||
for op in ('like_op', 'notlike_op', 'ilike_op',
|
||||
'notilike_op', 'between_op', 'in_op',
|
||||
'notin_op', 'endswith_op', 'concat_op'))
|
||||
for op in ('like_op', 'notlike_op', 'ilike_op',
|
||||
'notilike_op', 'between_op', 'in_op',
|
||||
'notin_op', 'endswith_op', 'concat_op'))
|
||||
|
||||
|
||||
class EvaluatorCompiler(object):
|
||||
def process(self, clause):
|
||||
meth = getattr(self, "visit_%s" % clause.__visit_name__, None)
|
||||
if not meth:
|
||||
raise UnevaluatableError("Cannot evaluate %s" % type(clause).__name__)
|
||||
raise UnevaluatableError(
|
||||
"Cannot evaluate %s" % type(clause).__name__)
|
||||
return meth(clause)
|
||||
|
||||
def visit_grouping(self, clause):
|
||||
@@ -39,6 +38,12 @@ class EvaluatorCompiler(object):
|
||||
def visit_null(self, clause):
|
||||
return lambda obj: None
|
||||
|
||||
def visit_false(self, clause):
|
||||
return lambda obj: False
|
||||
|
||||
def visit_true(self, clause):
|
||||
return lambda obj: True
|
||||
|
||||
def visit_column(self, clause):
|
||||
if 'parentmapper' in clause._annotations:
|
||||
key = clause._annotations['parentmapper'].\
|
||||
@@ -49,7 +54,7 @@ class EvaluatorCompiler(object):
|
||||
return lambda obj: get_corresponding_attr(obj)
|
||||
|
||||
def visit_clauselist(self, clause):
|
||||
evaluators = map(self.process, clause.clauses)
|
||||
evaluators = list(map(self.process, clause.clauses))
|
||||
if clause.operator is operators.or_:
|
||||
def evaluate(obj):
|
||||
has_null = False
|
||||
@@ -71,12 +76,15 @@ class EvaluatorCompiler(object):
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
raise UnevaluatableError("Cannot evaluate clauselist with operator %s" % clause.operator)
|
||||
raise UnevaluatableError(
|
||||
"Cannot evaluate clauselist with operator %s" %
|
||||
clause.operator)
|
||||
|
||||
return evaluate
|
||||
|
||||
def visit_binary(self, clause):
|
||||
eval_left,eval_right = map(self.process, [clause.left, clause.right])
|
||||
eval_left, eval_right = list(map(self.process,
|
||||
[clause.left, clause.right]))
|
||||
operator = clause.operator
|
||||
if operator is operators.is_:
|
||||
def evaluate(obj):
|
||||
@@ -92,7 +100,9 @@ class EvaluatorCompiler(object):
|
||||
return None
|
||||
return operator(eval_left(obj), eval_right(obj))
|
||||
else:
|
||||
raise UnevaluatableError("Cannot evaluate %s with operator %s" % (type(clause).__name__, clause.operator))
|
||||
raise UnevaluatableError(
|
||||
"Cannot evaluate %s with operator %s" %
|
||||
(type(clause).__name__, clause.operator))
|
||||
return evaluate
|
||||
|
||||
def visit_unary(self, clause):
|
||||
@@ -104,7 +114,9 @@ class EvaluatorCompiler(object):
|
||||
return None
|
||||
return not value
|
||||
return evaluate
|
||||
raise UnevaluatableError("Cannot evaluate %s with operator %s" % (type(clause).__name__, clause.operator))
|
||||
raise UnevaluatableError(
|
||||
"Cannot evaluate %s with operator %s" %
|
||||
(type(clause).__name__, clause.operator))
|
||||
|
||||
def visit_bindparam(self, clause):
|
||||
val = clause.value
|
||||
|
||||
+576
-176
File diff suppressed because it is too large
Load Diff
+35
-28
@@ -1,18 +1,17 @@
|
||||
# orm/exc.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 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
|
||||
|
||||
"""SQLAlchemy ORM exceptions."""
|
||||
|
||||
import sqlalchemy as sa
|
||||
orm_util = sa.util.importlater('sqlalchemy.orm', 'util')
|
||||
from .. import exc as sa_exc, util
|
||||
|
||||
NO_STATE = (AttributeError, KeyError)
|
||||
"""Exception types that may be raised by instrumentation implementations."""
|
||||
|
||||
class StaleDataError(sa.exc.SQLAlchemyError):
|
||||
|
||||
class StaleDataError(sa_exc.SQLAlchemyError):
|
||||
"""An operation encountered database state that is unaccounted for.
|
||||
|
||||
Conditions which cause this to happen include:
|
||||
@@ -41,31 +40,39 @@ class StaleDataError(sa.exc.SQLAlchemyError):
|
||||
ConcurrentModificationError = StaleDataError
|
||||
|
||||
|
||||
class FlushError(sa.exc.SQLAlchemyError):
|
||||
class FlushError(sa_exc.SQLAlchemyError):
|
||||
"""A invalid condition was detected during flush()."""
|
||||
|
||||
|
||||
class UnmappedError(sa.exc.InvalidRequestError):
|
||||
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):
|
||||
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."""
|
||||
|
||||
|
||||
class UnmappedInstanceError(UnmappedError):
|
||||
"""An mapping operation was requested for an unknown instance."""
|
||||
|
||||
def __init__(self, obj, msg=None):
|
||||
@util.dependencies("sqlalchemy.orm.base")
|
||||
def __init__(self, base, obj, msg=None):
|
||||
if not msg:
|
||||
try:
|
||||
mapper = sa.orm.class_mapper(type(obj))
|
||||
base.class_mapper(type(obj))
|
||||
name = _safe_cls_name(type(obj))
|
||||
msg = ("Class %r is mapped, but this instance lacks "
|
||||
"instrumentation. This occurs when the instance is created "
|
||||
"before sqlalchemy.orm.mapper(%s) was called." % (name, name))
|
||||
"instrumentation. This occurs when the instance"
|
||||
"is created before sqlalchemy.orm.mapper(%s) "
|
||||
"was called." % (name, name))
|
||||
except UnmappedClassError:
|
||||
msg = _default_unmapped(type(obj))
|
||||
if isinstance(obj, type):
|
||||
@@ -77,6 +84,7 @@ class UnmappedInstanceError(UnmappedError):
|
||||
def __reduce__(self):
|
||||
return self.__class__, (None, self.args[0])
|
||||
|
||||
|
||||
class UnmappedClassError(UnmappedError):
|
||||
"""An mapping operation was requested for an unknown class."""
|
||||
|
||||
@@ -88,7 +96,8 @@ class UnmappedClassError(UnmappedError):
|
||||
def __reduce__(self):
|
||||
return self.__class__, (None, self.args[0])
|
||||
|
||||
class ObjectDeletedError(sa.exc.InvalidRequestError):
|
||||
|
||||
class ObjectDeletedError(sa_exc.InvalidRequestError):
|
||||
"""A refresh operation failed to retrieve the database
|
||||
row corresponding to an object's known primary key identity.
|
||||
|
||||
@@ -107,33 +116,30 @@ class ObjectDeletedError(sa.exc.InvalidRequestError):
|
||||
object.
|
||||
|
||||
"""
|
||||
def __init__(self, state, msg=None):
|
||||
@util.dependencies("sqlalchemy.orm.base")
|
||||
def __init__(self, base, state, msg=None):
|
||||
if not msg:
|
||||
msg = "Instance '%s' has been deleted, or its "\
|
||||
"row is otherwise not present." % orm_util.state_str(state)
|
||||
"row is otherwise not present." % base.state_str(state)
|
||||
|
||||
sa.exc.InvalidRequestError.__init__(self, msg)
|
||||
sa_exc.InvalidRequestError.__init__(self, msg)
|
||||
|
||||
def __reduce__(self):
|
||||
return self.__class__, (None, self.args[0])
|
||||
|
||||
class UnmappedColumnError(sa.exc.InvalidRequestError):
|
||||
|
||||
class UnmappedColumnError(sa_exc.InvalidRequestError):
|
||||
"""Mapping operation was requested on an unknown column."""
|
||||
|
||||
|
||||
class NoResultFound(sa.exc.InvalidRequestError):
|
||||
class NoResultFound(sa_exc.InvalidRequestError):
|
||||
"""A database result was required but none was found."""
|
||||
|
||||
|
||||
class MultipleResultsFound(sa.exc.InvalidRequestError):
|
||||
class MultipleResultsFound(sa_exc.InvalidRequestError):
|
||||
"""A single database result was required but more than one were found."""
|
||||
|
||||
|
||||
# Legacy compat until 0.6.
|
||||
sa.exc.ConcurrentModificationError = ConcurrentModificationError
|
||||
sa.exc.FlushError = FlushError
|
||||
sa.exc.UnmappedColumnError
|
||||
|
||||
def _safe_cls_name(cls):
|
||||
try:
|
||||
cls_name = '.'.join((cls.__module__, cls.__name__))
|
||||
@@ -143,9 +149,10 @@ def _safe_cls_name(cls):
|
||||
cls_name = repr(cls)
|
||||
return cls_name
|
||||
|
||||
def _default_unmapped(cls):
|
||||
@util.dependencies("sqlalchemy.orm.base")
|
||||
def _default_unmapped(base, cls):
|
||||
try:
|
||||
mappers = sa.orm.attributes.manager_of_class(cls).mappers
|
||||
mappers = base.manager_of_class(cls).mappers
|
||||
except NO_STATE:
|
||||
mappers = {}
|
||||
except TypeError:
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
# orm/identity.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 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.orm import attributes
|
||||
|
||||
from . import attributes
|
||||
from .. import util
|
||||
|
||||
class IdentityMap(dict):
|
||||
def __init__(self):
|
||||
self._mutable_attrs = set()
|
||||
self._modified = set()
|
||||
self._wr = weakref.ref(self)
|
||||
|
||||
@@ -31,28 +30,20 @@ class IdentityMap(dict):
|
||||
|
||||
if state.modified:
|
||||
self._modified.add(state)
|
||||
if state.manager.mutable_attributes:
|
||||
self._mutable_attrs.add(state)
|
||||
|
||||
def _manage_removed_state(self, state):
|
||||
del state._instance_dict
|
||||
self._mutable_attrs.discard(state)
|
||||
self._modified.discard(state)
|
||||
|
||||
def _dirty_states(self):
|
||||
return self._modified.union(s for s in self._mutable_attrs.copy()
|
||||
if s.modified)
|
||||
return self._modified
|
||||
|
||||
def check_modified(self):
|
||||
"""return True if any InstanceStates present have been marked as 'modified'."""
|
||||
"""return True if any InstanceStates present have been marked
|
||||
as 'modified'.
|
||||
|
||||
if self._modified:
|
||||
return True
|
||||
else:
|
||||
for state in self._mutable_attrs.copy():
|
||||
if state.modified:
|
||||
return True
|
||||
return False
|
||||
"""
|
||||
return bool(self._modified)
|
||||
|
||||
def has_key(self, key):
|
||||
return key in self
|
||||
@@ -75,6 +66,7 @@ class IdentityMap(dict):
|
||||
def __delitem__(self, key):
|
||||
raise NotImplementedError("IdentityMap uses remove() to remove data")
|
||||
|
||||
|
||||
class WeakInstanceDict(IdentityMap):
|
||||
def __init__(self):
|
||||
IdentityMap.__init__(self)
|
||||
@@ -83,9 +75,7 @@ class WeakInstanceDict(IdentityMap):
|
||||
state = dict.__getitem__(self, key)
|
||||
o = state.obj()
|
||||
if o is None:
|
||||
o = state._is_really_none()
|
||||
if o is None:
|
||||
raise KeyError, key
|
||||
raise KeyError(key)
|
||||
return o
|
||||
|
||||
def __contains__(self, key):
|
||||
@@ -93,8 +83,6 @@ class WeakInstanceDict(IdentityMap):
|
||||
if dict.__contains__(self, key):
|
||||
state = dict.__getitem__(self, key)
|
||||
o = state.obj()
|
||||
if o is None:
|
||||
o = state._is_really_none()
|
||||
else:
|
||||
return False
|
||||
except KeyError:
|
||||
@@ -124,12 +112,11 @@ class WeakInstanceDict(IdentityMap):
|
||||
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, ))
|
||||
raise AssertionError(
|
||||
"A conflicting state is already "
|
||||
"present in the identity map for key %r"
|
||||
% (key, ))
|
||||
else:
|
||||
return
|
||||
except KeyError:
|
||||
@@ -143,9 +130,7 @@ class WeakInstanceDict(IdentityMap):
|
||||
return default
|
||||
o = state.obj()
|
||||
if o is None:
|
||||
o = state._is_really_none()
|
||||
if o is None:
|
||||
return default
|
||||
return default
|
||||
return o
|
||||
|
||||
def _items(self):
|
||||
@@ -167,28 +152,27 @@ class WeakInstanceDict(IdentityMap):
|
||||
|
||||
return result
|
||||
|
||||
# Py3K
|
||||
#def items(self):
|
||||
# return iter(self._items())
|
||||
#
|
||||
#def values(self):
|
||||
# return iter(self._values())
|
||||
# Py2K
|
||||
items = _items
|
||||
def iteritems(self):
|
||||
return iter(self.items())
|
||||
if util.py2k:
|
||||
items = _items
|
||||
values = _values
|
||||
|
||||
values = _values
|
||||
def itervalues(self):
|
||||
return iter(self.values())
|
||||
# end Py2K
|
||||
def iteritems(self):
|
||||
return iter(self.items())
|
||||
|
||||
def itervalues(self):
|
||||
return iter(self.values())
|
||||
else:
|
||||
def items(self):
|
||||
return iter(self._items())
|
||||
|
||||
def values(self):
|
||||
return iter(self._values())
|
||||
|
||||
def all_states(self):
|
||||
# Py3K
|
||||
# return list(dict.values(self))
|
||||
# Py2K
|
||||
return dict.values(self)
|
||||
# end Py2K
|
||||
if util.py2k:
|
||||
return dict.values(self)
|
||||
else:
|
||||
return list(dict.values(self))
|
||||
|
||||
def discard(self, state):
|
||||
st = dict.get(self, state.key, None)
|
||||
@@ -199,12 +183,15 @@ class WeakInstanceDict(IdentityMap):
|
||||
def prune(self):
|
||||
return 0
|
||||
|
||||
|
||||
class StrongInstanceDict(IdentityMap):
|
||||
def all_states(self):
|
||||
return [attributes.instance_state(o) for o in self.itervalues()]
|
||||
return [attributes.instance_state(o) for o in self.values()]
|
||||
|
||||
def contains_state(self, state):
|
||||
return state.key in self and attributes.instance_state(self[state.key]) is state
|
||||
return (
|
||||
state.key in self and
|
||||
attributes.instance_state(self[state.key]) is state)
|
||||
|
||||
def replace(self, state):
|
||||
if dict.__contains__(self, state.key):
|
||||
@@ -251,4 +238,3 @@ class StrongInstanceDict(IdentityMap):
|
||||
dict.update(self, keepers)
|
||||
self.modified = bool(dirty)
|
||||
return ref_count - len(self)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# orm/instrumentation.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 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
|
||||
@@ -14,79 +14,41 @@ 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.
|
||||
The class instrumentation system can be customized on a per-class
|
||||
or global basis using the :mod:`sqlalchemy.ext.instrumentation`
|
||||
module, which provides the means to build and specify
|
||||
alternate instrumentation forms.
|
||||
|
||||
.. versionchanged: 0.8
|
||||
The instrumentation extension system was moved out of the
|
||||
ORM and into the external :mod:`sqlalchemy.ext.instrumentation`
|
||||
package. When that package is imported, it installs
|
||||
itself within sqlalchemy.orm so that its more comprehensive
|
||||
resolution mechanics take effect.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
|
||||
from . import exc, collections, interfaces, state
|
||||
from .. import util
|
||||
from . import base
|
||||
|
||||
class ClassManager(dict):
|
||||
"""tracks state information at the class level."""
|
||||
|
||||
MANAGER_ATTR = '_sa_class_manager'
|
||||
STATE_ATTR = '_sa_instance_state'
|
||||
MANAGER_ATTR = base.DEFAULT_MANAGER_ATTR
|
||||
STATE_ATTR = base.DEFAULT_STATE_ATTR
|
||||
|
||||
deferred_scalar_loader = None
|
||||
|
||||
original_init = object.__init__
|
||||
|
||||
factory = None
|
||||
|
||||
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 = {}
|
||||
|
||||
@@ -99,10 +61,28 @@ class ClassManager(dict):
|
||||
for base in self._bases:
|
||||
self.update(base)
|
||||
|
||||
self.dispatch._events._new_classmanager_instance(class_, self)
|
||||
#events._InstanceEventsHold.populate(class_, self)
|
||||
|
||||
for basecls in class_.__mro__:
|
||||
mgr = manager_of_class(basecls)
|
||||
if mgr is not None:
|
||||
self.dispatch._update(mgr.dispatch)
|
||||
self.manage()
|
||||
self._instrument_init()
|
||||
|
||||
dispatch = event.dispatcher(events.InstanceEvents)
|
||||
if '__del__' in class_.__dict__:
|
||||
util.warn("__del__() method on class %s will "
|
||||
"cause unreachable cycles and memory leaks, "
|
||||
"as SQLAlchemy instrumentation often creates "
|
||||
"reference cycles. Please remove this method." %
|
||||
class_)
|
||||
|
||||
def __hash__(self):
|
||||
return id(self)
|
||||
|
||||
def __eq__(self, other):
|
||||
return other is self
|
||||
|
||||
@property
|
||||
def is_mapped(self):
|
||||
@@ -113,6 +93,24 @@ class ClassManager(dict):
|
||||
# raises unless self.mapper has been assigned
|
||||
raise exc.UnmappedClassError(self.class_)
|
||||
|
||||
def _all_sqla_attributes(self, exclude=None):
|
||||
"""return an iterator of all classbound attributes that are
|
||||
implement :class:`._InspectionAttr`.
|
||||
|
||||
This includes :class:`.QueryableAttribute` as well as extension
|
||||
types such as :class:`.hybrid_property` and :class:`.AssociationProxy`.
|
||||
|
||||
"""
|
||||
if exclude is None:
|
||||
exclude = set()
|
||||
for supercls in self.class_.__mro__:
|
||||
for key in set(supercls.__dict__).difference(exclude):
|
||||
exclude.add(key)
|
||||
val = supercls.__dict__[key]
|
||||
if isinstance(val, interfaces._InspectionAttr):
|
||||
yield key, val
|
||||
|
||||
|
||||
def _attr_has_impl(self, key):
|
||||
"""Return True if the given attribute is fully initialized.
|
||||
|
||||
@@ -134,7 +132,7 @@ class ClassManager(dict):
|
||||
"""
|
||||
manager = manager_of_class(cls)
|
||||
if manager is None:
|
||||
manager = _create_manager_for_cls(cls, _source=self)
|
||||
manager = _instrumentation_factory.create_manager_for_cls(cls)
|
||||
return manager
|
||||
|
||||
def _instrument_init(self):
|
||||
@@ -155,10 +153,7 @@ class ClassManager(dict):
|
||||
@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
|
||||
return state.InstanceState
|
||||
|
||||
def manage(self):
|
||||
"""Mark this instance as the manager for its class."""
|
||||
@@ -170,8 +165,25 @@ class ClassManager(dict):
|
||||
|
||||
delattr(self.class_, self.MANAGER_ATTR)
|
||||
|
||||
@util.hybridmethod
|
||||
def manager_getter(self):
|
||||
return attrgetter(self.MANAGER_ATTR)
|
||||
return _default_manager_getter
|
||||
|
||||
@util.hybridmethod
|
||||
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 _default_state_getter
|
||||
|
||||
@util.hybridmethod
|
||||
def dict_getter(self):
|
||||
return _default_dict_getter
|
||||
|
||||
|
||||
def instrument_attribute(self, key, inst, propagated=False):
|
||||
if propagated:
|
||||
@@ -196,7 +208,7 @@ class ClassManager(dict):
|
||||
yield m
|
||||
|
||||
def post_configure_attribute(self, key):
|
||||
instrumentation_registry.dispatch.\
|
||||
_instrumentation_factory.dispatch.\
|
||||
attribute_instrument(self.class_, key, self[key])
|
||||
|
||||
def uninstrument_attribute(self, key, propagated=False):
|
||||
@@ -209,8 +221,6 @@ class ClassManager(dict):
|
||||
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:
|
||||
@@ -271,7 +281,7 @@ class ClassManager(dict):
|
||||
|
||||
@property
|
||||
def attributes(self):
|
||||
return self.itervalues()
|
||||
return iter(self.values())
|
||||
|
||||
## InstanceState management
|
||||
|
||||
@@ -288,6 +298,9 @@ class ClassManager(dict):
|
||||
def teardown_instance(self, instance):
|
||||
delattr(instance, self.STATE_ATTR)
|
||||
|
||||
def _serialize(self, state, state_dict):
|
||||
return _SerializeManager(state, state_dict)
|
||||
|
||||
def _new_state_if_none(self, instance):
|
||||
"""Install a default InstanceState if none is present.
|
||||
|
||||
@@ -310,19 +323,6 @@ class ClassManager(dict):
|
||||
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)
|
||||
|
||||
@@ -330,123 +330,116 @@ class ClassManager(dict):
|
||||
"""TODO"""
|
||||
return self.get_impl(key).hasparent(state, optimistic=optimistic)
|
||||
|
||||
def __nonzero__(self):
|
||||
def __bool__(self):
|
||||
"""All ClassManagers are non-zero regardless of attribute state."""
|
||||
return True
|
||||
|
||||
__nonzero__ = __bool__
|
||||
|
||||
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."""
|
||||
class _SerializeManager(object):
|
||||
"""Provide serialization of a :class:`.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_)
|
||||
The :class:`.InstanceState` uses ``__init__()`` on serialize
|
||||
and ``__call__()`` on deserialize.
|
||||
|
||||
ClassManager.__init__(self, class_, **kw)
|
||||
"""
|
||||
def __init__(self, state, d):
|
||||
self.class_ = state.class_
|
||||
manager = state.manager
|
||||
manager.dispatch.pickle(state, d)
|
||||
|
||||
def manage(self):
|
||||
self._adapted.manage(self.class_, self)
|
||||
def __call__(self, state, inst, state_dict):
|
||||
state.manager = manager = manager_of_class(self.class_)
|
||||
if manager is None:
|
||||
raise exc.UnmappedInstanceError(
|
||||
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.configured:
|
||||
manager.mapper._configure_all()
|
||||
|
||||
def dispose(self):
|
||||
self._adapted.dispose(self.class_)
|
||||
# setup _sa_instance_state ahead of time so that
|
||||
# unpickle events can access the object normally.
|
||||
# see [ticket:2362]
|
||||
if inst is not None:
|
||||
manager.setup_instance(inst, state)
|
||||
manager.dispatch.unpickle(state, state_dict)
|
||||
|
||||
def manager_getter(self):
|
||||
return self._adapted.manager_getter(self.class_)
|
||||
class InstrumentationFactory(object):
|
||||
"""Factory for new ClassManager instances."""
|
||||
|
||||
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 create_manager_for_cls(self, class_):
|
||||
assert class_ is not None
|
||||
assert manager_of_class(class_) is None
|
||||
|
||||
def post_configure_attribute(self, key):
|
||||
super(_ClassInstrumentationAdapter, self).post_configure_attribute(key)
|
||||
self._adapted.post_configure_attribute(self.class_, key, self[key])
|
||||
# give a more complicated subclass
|
||||
# a chance to do what it wants here
|
||||
manager, factory = self._locate_extended_factory(class_)
|
||||
|
||||
def install_descriptor(self, key, inst):
|
||||
self._adapted.install_descriptor(self.class_, key, inst)
|
||||
if factory is None:
|
||||
factory = ClassManager
|
||||
manager = factory(class_)
|
||||
|
||||
def uninstall_descriptor(self, key):
|
||||
self._adapted.uninstall_descriptor(self.class_, key)
|
||||
self._check_conflicts(class_, factory)
|
||||
|
||||
def install_member(self, key, implementation):
|
||||
self._adapted.install_member(self.class_, key, implementation)
|
||||
manager.factory = factory
|
||||
|
||||
def uninstall_member(self, key):
|
||||
self._adapted.uninstall_member(self.class_, key)
|
||||
self.dispatch.class_instrument(class_)
|
||||
return manager
|
||||
|
||||
def instrument_collection_class(self, key, collection_class):
|
||||
return self._adapted.instrument_collection_class(
|
||||
self.class_, key, collection_class)
|
||||
def _locate_extended_factory(self, class_):
|
||||
"""Overridden by a subclass to do an extended lookup."""
|
||||
return None, None
|
||||
|
||||
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 _check_conflicts(self, class_, factory):
|
||||
"""Overridden by a subclass to test for conflicting factories."""
|
||||
return
|
||||
|
||||
def new_instance(self, state=None):
|
||||
instance = self.class_.__new__(self.class_)
|
||||
self.setup_instance(instance, state)
|
||||
return instance
|
||||
def unregister(self, class_):
|
||||
manager = manager_of_class(class_)
|
||||
manager.unregister()
|
||||
manager.dispose()
|
||||
self.dispatch.class_uninstrument(class_)
|
||||
if ClassManager.MANAGER_ATTR in class_.__dict__:
|
||||
delattr(class_, ClassManager.MANAGER_ATTR)
|
||||
|
||||
def _new_state_if_none(self, instance):
|
||||
"""Install a default InstanceState if none is present.
|
||||
# this attribute is replaced by sqlalchemy.ext.instrumentation
|
||||
# when importred.
|
||||
_instrumentation_factory = InstrumentationFactory()
|
||||
|
||||
A private convenience method used by the __init__ decorator.
|
||||
"""
|
||||
if self.has_state(instance):
|
||||
return False
|
||||
else:
|
||||
return self.setup_instance(instance)
|
||||
# these attributes are replaced by sqlalchemy.ext.instrumentation
|
||||
# when a non-standard InstrumentationManager class is first
|
||||
# used to instrument a class.
|
||||
instance_state = _default_state_getter = base.instance_state
|
||||
|
||||
def setup_instance(self, instance, state=None):
|
||||
self._adapted.initialize_instance_dict(self.class_, instance)
|
||||
instance_dict = _default_dict_getter = base.instance_dict
|
||||
|
||||
if state is None:
|
||||
state = self._state_constructor(instance, self)
|
||||
manager_of_class = _default_manager_getter = base.manager_of_class
|
||||
|
||||
# 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):
|
||||
def register_class(class_):
|
||||
"""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)
|
||||
manager = _instrumentation_factory.create_manager_for_cls(class_)
|
||||
return manager
|
||||
|
||||
|
||||
def unregister_class(class_):
|
||||
"""Unregister class instrumentation."""
|
||||
|
||||
instrumentation_registry.unregister(class_)
|
||||
_instrumentation_factory.unregister(class_)
|
||||
|
||||
|
||||
def is_instrumented(instance, key):
|
||||
@@ -460,174 +453,6 @@ def is_instrumented(instance, key):
|
||||
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."""
|
||||
@@ -647,28 +472,28 @@ def _generate_init(class_, class_manager):
|
||||
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)
|
||||
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
|
||||
if util.py2k:
|
||||
func = getattr(original__init__, 'im_func', original__init__)
|
||||
func_defaults = getattr(func, 'func_defaults', None)
|
||||
else:
|
||||
func_defaults = getattr(original__init__, '__defaults__', None)
|
||||
func_kw_defaults = getattr(original__init__, '__kwdefaults__', None)
|
||||
|
||||
env = locals().copy()
|
||||
exec func_text in env
|
||||
exec(func_text, 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
|
||||
__init__.__defaults__ = func_defaults
|
||||
if not util.py2k and func_kw_defaults:
|
||||
__init__.__kwdefaults__ = func_kw_defaults
|
||||
|
||||
return __init__
|
||||
|
||||
+233
-439
@@ -1,5 +1,5 @@
|
||||
# orm/interfaces.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 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
|
||||
@@ -16,16 +16,16 @@ classes within should be considered mostly private.
|
||||
|
||||
"""
|
||||
|
||||
from itertools import chain
|
||||
from __future__ import absolute_import
|
||||
|
||||
from sqlalchemy import exc as sa_exc
|
||||
from sqlalchemy import util
|
||||
from sqlalchemy.sql import operators
|
||||
deque = __import__('collections').deque
|
||||
from .. import exc as sa_exc, util, inspect
|
||||
from ..sql import operators
|
||||
from collections import deque
|
||||
from .base import ONETOMANY, MANYTOONE, MANYTOMANY, EXT_CONTINUE, EXT_STOP, NOT_EXTENSION
|
||||
from .base import _InspectionAttr, _MappedAttribute
|
||||
from .path_registry import PathRegistry
|
||||
import collections
|
||||
|
||||
mapperutil = util.importlater('sqlalchemy.orm', 'util')
|
||||
|
||||
collections = None
|
||||
|
||||
__all__ = (
|
||||
'AttributeExtension',
|
||||
@@ -42,21 +42,11 @@ __all__ = (
|
||||
'SessionExtension',
|
||||
'StrategizedOption',
|
||||
'StrategizedProperty',
|
||||
'build_path',
|
||||
)
|
||||
|
||||
EXT_CONTINUE = util.symbol('EXT_CONTINUE')
|
||||
EXT_STOP = util.symbol('EXT_STOP')
|
||||
|
||||
ONETOMANY = util.symbol('ONETOMANY')
|
||||
MANYTOONE = util.symbol('MANYTOONE')
|
||||
MANYTOMANY = util.symbol('MANYTOMANY')
|
||||
|
||||
from deprecated_interfaces import AttributeExtension, SessionExtension, \
|
||||
MapperExtension
|
||||
|
||||
|
||||
class MapperProperty(object):
|
||||
class MapperProperty(_MappedAttribute, _InspectionAttr):
|
||||
"""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,
|
||||
@@ -66,18 +56,21 @@ class MapperProperty(object):
|
||||
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`.
|
||||
represented in the mapping as an instance of
|
||||
:class:`.RelationshipProperty`.
|
||||
|
||||
"""
|
||||
|
||||
cascade = ()
|
||||
cascade = frozenset()
|
||||
"""The set of 'cascade' attribute names.
|
||||
|
||||
This collection is checked before the 'cascade_iterator' method is called.
|
||||
|
||||
"""
|
||||
|
||||
def setup(self, context, entity, path, reduced_path, adapter, **kwargs):
|
||||
is_property = True
|
||||
|
||||
def setup(self, context, entity, path, adapter, **kwargs):
|
||||
"""Called by Query for the purposes of constructing a SQL statement.
|
||||
|
||||
Each MapperProperty associated with the target mapper processes the
|
||||
@@ -87,7 +80,7 @@ class MapperProperty(object):
|
||||
|
||||
pass
|
||||
|
||||
def create_row_processor(self, context, path, reduced_path,
|
||||
def create_row_processor(self, context, path,
|
||||
mapper, row, adapter):
|
||||
"""Return a 3-tuple consisting of three row processing functions.
|
||||
|
||||
@@ -112,11 +105,33 @@ class MapperProperty(object):
|
||||
def set_parent(self, parent, init):
|
||||
self.parent = parent
|
||||
|
||||
def instrument_class(self, mapper):
|
||||
def instrument_class(self, mapper): # pragma: no-coverage
|
||||
raise NotImplementedError()
|
||||
|
||||
_compile_started = False
|
||||
_compile_finished = False
|
||||
@util.memoized_property
|
||||
def info(self):
|
||||
"""Info dictionary associated with the object, allowing user-defined
|
||||
data to be associated with this :class:`.MapperProperty`.
|
||||
|
||||
The dictionary is generated when first accessed. Alternatively,
|
||||
it can be specified as a constructor argument to the
|
||||
:func:`.column_property`, :func:`.relationship`, or :func:`.composite`
|
||||
functions.
|
||||
|
||||
.. versionadded:: 0.8 Added support for .info to all
|
||||
:class:`.MapperProperty` subclasses.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:attr:`.QueryableAttribute.info`
|
||||
|
||||
:attr:`.SchemaItem.info`
|
||||
|
||||
"""
|
||||
return {}
|
||||
|
||||
_configure_started = False
|
||||
_configure_finished = False
|
||||
|
||||
def init(self):
|
||||
"""Called after all mappers are created to assemble
|
||||
@@ -124,14 +139,33 @@ class MapperProperty(object):
|
||||
initialization steps.
|
||||
|
||||
"""
|
||||
self._compile_started = True
|
||||
self._configure_started = True
|
||||
self.do_init()
|
||||
self._compile_finished = True
|
||||
self._configure_finished = True
|
||||
|
||||
@property
|
||||
def class_attribute(self):
|
||||
"""Return the class-bound descriptor corresponding to this
|
||||
MapperProperty."""
|
||||
:class:`.MapperProperty`.
|
||||
|
||||
This is basically a ``getattr()`` call::
|
||||
|
||||
return getattr(self.parent.class_, self.key)
|
||||
|
||||
I.e. if this :class:`.MapperProperty` were named ``addresses``,
|
||||
and the class to which it is mapped is ``User``, this sequence
|
||||
is possible::
|
||||
|
||||
>>> from sqlalchemy import inspect
|
||||
>>> mapper = inspect(User)
|
||||
>>> addresses_property = mapper.attrs.addresses
|
||||
>>> addresses_property.class_attribute is User.addresses
|
||||
True
|
||||
>>> User.addresses.property is addresses_property
|
||||
True
|
||||
|
||||
|
||||
"""
|
||||
|
||||
return getattr(self.parent.class_, self.key)
|
||||
|
||||
@@ -153,9 +187,6 @@ class MapperProperty(object):
|
||||
"""
|
||||
pass
|
||||
|
||||
def per_property_preprocessors(self, uow):
|
||||
pass
|
||||
|
||||
def is_primary(self):
|
||||
"""Return True if this ``MapperProperty``'s mapper is the
|
||||
primary mapper for its class.
|
||||
@@ -186,47 +217,131 @@ class MapperProperty(object):
|
||||
|
||||
return operator(self.comparator, value)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s at 0x%x; %s>' % (
|
||||
self.__class__.__name__,
|
||||
id(self), getattr(self, 'key', 'no key'))
|
||||
|
||||
class PropComparator(operators.ColumnOperators):
|
||||
"""Defines comparison operations for MapperProperty objects.
|
||||
"""Defines boolean, comparison, and other operators for
|
||||
:class:`.MapperProperty` objects.
|
||||
|
||||
SQLAlchemy allows for operators to
|
||||
be redefined at both the Core and ORM level. :class:`.PropComparator`
|
||||
is the base class of operator redefinition for ORM-level operations,
|
||||
including those of :class:`.ColumnProperty`,
|
||||
:class:`.RelationshipProperty`, and :class:`.CompositeProperty`.
|
||||
|
||||
.. note:: With the advent of Hybrid properties introduced in SQLAlchemy
|
||||
0.7, as well as Core-level operator redefinition in
|
||||
SQLAlchemy 0.8, the use case for user-defined :class:`.PropComparator`
|
||||
instances is extremely rare. See :ref:`hybrids_toplevel` as well
|
||||
as :ref:`types_operators`.
|
||||
|
||||
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
|
||||
:meth:`.operators.ColumnOperators.__eq__`,
|
||||
:meth:`.operators.ColumnOperators.__lt__`, and
|
||||
:meth:`.operators.ColumnOperators.__add__`, can be overridden to provide
|
||||
new operator behavior. The custom :class:`.PropComparator` is passed to
|
||||
the mapper property via the ``comparator_factory`` argument. In each case,
|
||||
the :class:`.MapperProperty` instance via the ``comparator_factory``
|
||||
argument. In each case,
|
||||
the appropriate subclass of :class:`.PropComparator` should be used::
|
||||
|
||||
# definition of custom PropComparator subclasses
|
||||
|
||||
from sqlalchemy.orm.properties import \\
|
||||
ColumnProperty,\\
|
||||
CompositeProperty,\\
|
||||
RelationshipProperty
|
||||
|
||||
class MyColumnComparator(ColumnProperty.Comparator):
|
||||
pass
|
||||
|
||||
class MyCompositeComparator(CompositeProperty.Comparator):
|
||||
pass
|
||||
def __eq__(self, other):
|
||||
return self.__clause_element__() == other
|
||||
|
||||
class MyRelationshipComparator(RelationshipProperty.Comparator):
|
||||
pass
|
||||
def any(self, expression):
|
||||
"define the 'any' operation"
|
||||
# ...
|
||||
|
||||
class MyCompositeComparator(CompositeProperty.Comparator):
|
||||
def __gt__(self, other):
|
||||
"redefine the 'greater than' operation"
|
||||
|
||||
return sql.and_(*[a>b for a, b in
|
||||
zip(self.__clause_element__().clauses,
|
||||
other.__composite_values__())])
|
||||
|
||||
|
||||
# application of custom PropComparator subclasses
|
||||
|
||||
from sqlalchemy.orm import column_property, relationship, composite
|
||||
from sqlalchemy import Column, String
|
||||
|
||||
class SomeMappedClass(Base):
|
||||
some_column = column_property(Column("some_column", String),
|
||||
comparator_factory=MyColumnComparator)
|
||||
|
||||
some_relationship = relationship(SomeOtherClass,
|
||||
comparator_factory=MyRelationshipComparator)
|
||||
|
||||
some_composite = composite(
|
||||
Column("a", String), Column("b", String),
|
||||
comparator_factory=MyCompositeComparator
|
||||
)
|
||||
|
||||
Note that for column-level operator redefinition, it's usually
|
||||
simpler to define the operators at the Core level, using the
|
||||
:attr:`.TypeEngine.comparator_factory` attribute. See
|
||||
:ref:`types_operators` for more detail.
|
||||
|
||||
See also:
|
||||
|
||||
:class:`.ColumnProperty.Comparator`
|
||||
|
||||
:class:`.RelationshipProperty.Comparator`
|
||||
|
||||
:class:`.CompositeProperty.Comparator`
|
||||
|
||||
:class:`.ColumnOperators`
|
||||
|
||||
:ref:`types_operators`
|
||||
|
||||
:attr:`.TypeEngine.comparator_factory`
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, prop, mapper, adapter=None):
|
||||
def __init__(self, prop, parentmapper, adapt_to_entity=None):
|
||||
self.prop = self.property = prop
|
||||
self.mapper = mapper
|
||||
self.adapter = adapter
|
||||
self._parentmapper = parentmapper
|
||||
self._adapt_to_entity = adapt_to_entity
|
||||
|
||||
def __clause_element__(self):
|
||||
raise NotImplementedError("%r" % self)
|
||||
|
||||
def adapted(self, adapter):
|
||||
def _query_clause_element(self):
|
||||
return self.__clause_element__()
|
||||
|
||||
def adapt_to_entity(self, adapt_to_entity):
|
||||
"""Return a copy of this PropComparator which will use the given
|
||||
adaption function on the local side of generated expressions.
|
||||
:class:`.AliasedInsp` to produce corresponding expressions.
|
||||
"""
|
||||
return self.__class__(self.prop, self._parentmapper, adapt_to_entity)
|
||||
|
||||
@property
|
||||
def adapter(self):
|
||||
"""Produce a callable that adapts column expressions
|
||||
to suit an aliased version of this comparator.
|
||||
|
||||
"""
|
||||
if self._adapt_to_entity is None:
|
||||
return None
|
||||
else:
|
||||
return self._adapt_to_entity._adapt_element
|
||||
|
||||
return self.__class__(self.prop, self.mapper, adapter)
|
||||
@util.memoized_property
|
||||
def info(self):
|
||||
return self.property.info
|
||||
|
||||
@staticmethod
|
||||
def any_op(a, b, **kwargs):
|
||||
@@ -251,8 +366,8 @@ class PropComparator(operators.ColumnOperators):
|
||||
query.join(Company.employees.of_type(Engineer)).\\
|
||||
filter(Engineer.name=='foo')
|
||||
|
||||
:param \class_: a class or mapper indicating that criterion will be against
|
||||
this specific subclass.
|
||||
:param \class_: a class or mapper indicating that criterion will be
|
||||
against this specific subclass.
|
||||
|
||||
|
||||
"""
|
||||
@@ -269,9 +384,9 @@ class PropComparator(operators.ColumnOperators):
|
||||
: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.
|
||||
:param \**kwargs: key/value pairs corresponding to member class
|
||||
attribute names which will be compared via equality to the
|
||||
corresponding values.
|
||||
|
||||
"""
|
||||
|
||||
@@ -287,9 +402,9 @@ class PropComparator(operators.ColumnOperators):
|
||||
: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.
|
||||
:param \**kwargs: key/value pairs corresponding to member class
|
||||
attribute names which will be compared via equality to the
|
||||
corresponding values.
|
||||
|
||||
"""
|
||||
|
||||
@@ -308,75 +423,87 @@ class StrategizedProperty(MapperProperty):
|
||||
|
||||
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]
|
||||
def _get_context_loader(self, context, path):
|
||||
load = None
|
||||
|
||||
if cls:
|
||||
try:
|
||||
return self._strategies[cls]
|
||||
except KeyError:
|
||||
return self.__init_strategy(cls)
|
||||
return self.strategy
|
||||
# use EntityRegistry.__getitem__()->PropRegistry here so
|
||||
# that the path is stated in terms of our base
|
||||
search_path = dict.__getitem__(path, self)
|
||||
|
||||
def _get_strategy(self, cls):
|
||||
# search among: exact match, "attr.*", "default" strategy
|
||||
# if any.
|
||||
for path_key in (
|
||||
search_path._loader_key,
|
||||
search_path._wildcard_path_loader_key,
|
||||
search_path._default_path_loader_key
|
||||
):
|
||||
if path_key in context.attributes:
|
||||
load = context.attributes[path_key]
|
||||
break
|
||||
|
||||
return load
|
||||
|
||||
def _get_strategy(self, key):
|
||||
try:
|
||||
return self._strategies[cls]
|
||||
return self._strategies[key]
|
||||
except KeyError:
|
||||
return self.__init_strategy(cls)
|
||||
cls = self._strategy_lookup(*key)
|
||||
self._strategies[key] = self._strategies[cls] = strategy = cls(self)
|
||||
return strategy
|
||||
|
||||
def __init_strategy(self, cls):
|
||||
self._strategies[cls] = strategy = cls(self)
|
||||
return strategy
|
||||
def _get_strategy_by_cls(self, cls):
|
||||
return self._get_strategy(cls._strategy_keys[0])
|
||||
|
||||
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 setup(self, context, entity, path, adapter, **kwargs):
|
||||
loader = self._get_context_loader(context, path)
|
||||
if loader and loader.strategy:
|
||||
strat = self._get_strategy(loader.strategy)
|
||||
else:
|
||||
strat = self.strategy
|
||||
strat.setup_query(context, entity, path, loader, adapter, **kwargs)
|
||||
|
||||
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 create_row_processor(self, context, path, mapper, row, adapter):
|
||||
loader = self._get_context_loader(context, path)
|
||||
if loader and loader.strategy:
|
||||
strat = self._get_strategy(loader.strategy)
|
||||
else:
|
||||
strat = self.strategy
|
||||
return strat.create_row_processor(context, path, loader,
|
||||
mapper, row, adapter)
|
||||
|
||||
def do_init(self):
|
||||
self._strategies = {}
|
||||
self.strategy = self.__init_strategy(self.strategy_class)
|
||||
self.strategy = self._get_strategy_by_cls(self.strategy_class)
|
||||
|
||||
def post_instrument_class(self, mapper):
|
||||
if self.is_primary() and \
|
||||
not mapper.class_manager._attr_has_impl(self.key):
|
||||
self.strategy.init_class_attribute(mapper)
|
||||
|
||||
def build_path(entity, key, prev=None):
|
||||
if prev:
|
||||
return prev + (entity, key)
|
||||
else:
|
||||
return (entity, key)
|
||||
|
||||
def serialize_path(path):
|
||||
if path is None:
|
||||
return None
|
||||
_strategies = collections.defaultdict(dict)
|
||||
|
||||
return zip(
|
||||
[m.class_ for m in [path[i] for i in range(0, len(path), 2)]],
|
||||
[path[i] for i in range(1, len(path), 2)] + [None]
|
||||
)
|
||||
@classmethod
|
||||
def strategy_for(cls, **kw):
|
||||
def decorate(dec_cls):
|
||||
dec_cls._strategy_keys = []
|
||||
key = tuple(sorted(kw.items()))
|
||||
cls._strategies[cls][key] = dec_cls
|
||||
dec_cls._strategy_keys.append(key)
|
||||
return dec_cls
|
||||
return decorate
|
||||
|
||||
def deserialize_path(path):
|
||||
if path is None:
|
||||
return None
|
||||
@classmethod
|
||||
def _strategy_lookup(cls, *key):
|
||||
for prop_cls in cls.__mro__:
|
||||
if prop_cls in cls._strategies:
|
||||
strategies = cls._strategies[prop_cls]
|
||||
try:
|
||||
return strategies[key]
|
||||
except KeyError:
|
||||
pass
|
||||
raise Exception("can't locate strategy for %s %s" % (cls, key))
|
||||
|
||||
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
|
||||
|
||||
class MapperOption(object):
|
||||
"""Describe a modification to a Query."""
|
||||
@@ -398,242 +525,8 @@ class MapperOption(object):
|
||||
|
||||
self.process_query(query)
|
||||
|
||||
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
|
||||
or list of class-bound attributes. """
|
||||
|
||||
def __init__(self, key, mapper=None):
|
||||
self.key = key
|
||||
self.mapper = mapper
|
||||
|
||||
def process_query(self, query):
|
||||
self._process(query, True)
|
||||
|
||||
def process_query_conditionally(self, query):
|
||||
self._process(query, False)
|
||||
|
||||
def _process(self, query, raiseerr):
|
||||
paths, mappers = self._get_paths(query, raiseerr)
|
||||
if paths:
|
||||
self.process_query_property(query, paths, mappers)
|
||||
|
||||
def process_query_property(self, query, paths, mappers):
|
||||
pass
|
||||
|
||||
def __getstate__(self):
|
||||
d = self.__dict__.copy()
|
||||
d['key'] = ret = []
|
||||
for token in util.to_list(self.key):
|
||||
if isinstance(token, PropComparator):
|
||||
ret.append((token.mapper.class_, token.key))
|
||||
else:
|
||||
ret.append(token)
|
||||
return d
|
||||
|
||||
def __setstate__(self, state):
|
||||
ret = []
|
||||
for key in state['key']:
|
||||
if isinstance(key, tuple):
|
||||
cls, propkey = key
|
||||
ret.append(getattr(cls, propkey))
|
||||
else:
|
||||
ret.append(key)
|
||||
state['key'] = tuple(ret)
|
||||
self.__dict__ = state
|
||||
|
||||
def _find_entity_prop_comparator(self, query, token, mapper, raiseerr):
|
||||
if mapperutil._is_aliased_class(mapper):
|
||||
searchfor = mapper
|
||||
isa = False
|
||||
else:
|
||||
searchfor = mapperutil._class_to_mapper(mapper)
|
||||
isa = True
|
||||
for ent in query._mapper_entities:
|
||||
if searchfor is ent.path_entity or isa \
|
||||
and searchfor.common_parent(ent.path_entity):
|
||||
return ent
|
||||
else:
|
||||
if raiseerr:
|
||||
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
|
||||
|
||||
def _get_paths(self, query, raiseerr):
|
||||
path = None
|
||||
entity = None
|
||||
l = []
|
||||
mappers = []
|
||||
|
||||
# _current_path implies we're in a
|
||||
# secondary load with an existing path
|
||||
current_path = list(query._current_path)
|
||||
|
||||
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:
|
||||
entity = self._find_entity_basestring(
|
||||
query,
|
||||
token,
|
||||
raiseerr)
|
||||
if entity is None:
|
||||
return [], []
|
||||
path_element = entity.path_entity
|
||||
mapper = entity.mapper
|
||||
mappers.append(mapper)
|
||||
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:
|
||||
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)
|
||||
else:
|
||||
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 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 StrategizedOption(PropertyOption):
|
||||
"""A MapperOption that affects which LoaderStrategy will be used
|
||||
for an operation by a StrategizedProperty.
|
||||
"""
|
||||
|
||||
chained = False
|
||||
|
||||
def process_query_property(self, query, paths, mappers):
|
||||
|
||||
# _get_context_strategy may receive the path in terms of a base
|
||||
# mapper - e.g. options(eagerload_all(Company.employees,
|
||||
# Engineer.machines)) in the polymorphic tests leads to
|
||||
# "(Person, 'machines')" in the path due to the mechanics of how
|
||||
# the eager strategy builds up the path
|
||||
|
||||
if self.chained:
|
||||
for path in paths:
|
||||
query._attributes[('loaderstrategy',
|
||||
_reduce_path(path))] = \
|
||||
self.get_strategy_class()
|
||||
else:
|
||||
query._attributes[('loaderstrategy',
|
||||
_reduce_path(paths[-1]))] = \
|
||||
self.get_strategy_class()
|
||||
|
||||
def get_strategy_class(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _reduce_path(path):
|
||||
"""Convert a (mapper, path) path to use base mappers.
|
||||
|
||||
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 referenced by Mapper.prop1.
|
||||
|
||||
"""
|
||||
return tuple([i % 2 != 0 and
|
||||
element or
|
||||
getattr(element, 'base_mapper', element)
|
||||
for i, element in enumerate(path)])
|
||||
|
||||
class LoaderStrategy(object):
|
||||
"""Describe the loading behavior of a StrategizedProperty object.
|
||||
@@ -663,22 +556,14 @@ class LoaderStrategy(object):
|
||||
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")
|
||||
|
||||
def init_class_attribute(self, mapper):
|
||||
pass
|
||||
|
||||
def setup_query(self, context, entity, path, reduced_path, adapter, **kwargs):
|
||||
def setup_query(self, context, entity, path, loadopt, adapter, **kwargs):
|
||||
pass
|
||||
|
||||
def create_row_processor(self, context, path, reduced_path, mapper,
|
||||
def create_row_processor(self, context, path, loadopt, mapper,
|
||||
row, adapter):
|
||||
"""Return row processing functions which fulfill the contract
|
||||
specified by MapperProperty.create_row_processor.
|
||||
@@ -690,94 +575,3 @@ class LoaderStrategy(object):
|
||||
|
||||
def __str__(self):
|
||||
return str(self.parent_property)
|
||||
|
||||
def debug_callable(self, fn, logger, announcement, logfn):
|
||||
if announcement:
|
||||
logger.debug(announcement)
|
||||
if logfn:
|
||||
def call(*args, **kwargs):
|
||||
logger.debug(logfn(*args, **kwargs))
|
||||
return fn(*args, **kwargs)
|
||||
return call
|
||||
else:
|
||||
return fn
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
|
||||
# r4361 added a mandatory (cls) constructor to this interface.
|
||||
# given that, perhaps class_ should be dropped from all of these
|
||||
# signatures.
|
||||
|
||||
def __init__(self, class_):
|
||||
pass
|
||||
|
||||
def manage(self, class_, manager):
|
||||
setattr(class_, '_default_class_manager', manager)
|
||||
|
||||
def dispose(self, class_, manager):
|
||||
delattr(class_, '_default_class_manager')
|
||||
|
||||
def manager_getter(self, class_):
|
||||
def get(cls):
|
||||
return cls._default_class_manager
|
||||
return get
|
||||
|
||||
def instrument_attribute(self, class_, key, inst):
|
||||
pass
|
||||
|
||||
def post_configure_attribute(self, class_, key, inst):
|
||||
pass
|
||||
|
||||
def install_descriptor(self, class_, key, inst):
|
||||
setattr(class_, key, inst)
|
||||
|
||||
def uninstall_descriptor(self, class_, key):
|
||||
delattr(class_, key)
|
||||
|
||||
def install_member(self, class_, key, implementation):
|
||||
setattr(class_, key, implementation)
|
||||
|
||||
def uninstall_member(self, class_, key):
|
||||
delattr(class_, key)
|
||||
|
||||
def instrument_collection_class(self, class_, key, collection_class):
|
||||
global collections
|
||||
if collections is None:
|
||||
from sqlalchemy.orm import collections
|
||||
return collections.prepare_instrumentation(collection_class)
|
||||
|
||||
def get_instance_dict(self, class_, instance):
|
||||
return instance.__dict__
|
||||
|
||||
def initialize_instance_dict(self, class_, instance):
|
||||
pass
|
||||
|
||||
def install_state(self, class_, instance, state):
|
||||
setattr(instance, '_default_state', state)
|
||||
|
||||
def remove_state(self, class_, instance):
|
||||
delattr(instance, '_default_state')
|
||||
|
||||
def state_getter(self, class_):
|
||||
return lambda instance: getattr(instance, '_default_state')
|
||||
|
||||
def dict_getter(self, class_):
|
||||
return lambda inst: self.get_instance_dict(class_, inst)
|
||||
|
||||
@@ -0,0 +1,610 @@
|
||||
# orm/loading.py
|
||||
# Copyright (C) 2005-2014 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
|
||||
|
||||
"""private module containing functions used to convert database
|
||||
rows into object instances and associated state.
|
||||
|
||||
the functions here are called primarily by Query, Mapper,
|
||||
as well as some of the attribute loading strategies.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
from .. import util
|
||||
from . import attributes, exc as orm_exc, state as statelib
|
||||
from .interfaces import EXT_CONTINUE
|
||||
from ..sql import util as sql_util
|
||||
from .util import _none_set, state_str
|
||||
from .. import exc as sa_exc
|
||||
|
||||
_new_runid = util.counter()
|
||||
|
||||
|
||||
def instances(query, cursor, context):
|
||||
"""Return an ORM result as an iterator."""
|
||||
session = query.session
|
||||
|
||||
context.runid = _new_runid()
|
||||
|
||||
filter_fns = [ent.filter_fn
|
||||
for ent in query._entities]
|
||||
filtered = id in filter_fns
|
||||
|
||||
single_entity = len(query._entities) == 1 and \
|
||||
query._entities[0].supports_single_entity
|
||||
|
||||
if filtered:
|
||||
if single_entity:
|
||||
filter_fn = id
|
||||
else:
|
||||
def filter_fn(row):
|
||||
return tuple(fn(x) for x, fn in zip(row, filter_fns))
|
||||
|
||||
custom_rows = single_entity and \
|
||||
query._entities[0].custom_rows
|
||||
|
||||
(process, labels) = \
|
||||
list(zip(*[
|
||||
query_entity.row_processor(query,
|
||||
context, custom_rows)
|
||||
for query_entity in query._entities
|
||||
]))
|
||||
|
||||
while True:
|
||||
context.progress = {}
|
||||
context.partials = {}
|
||||
|
||||
if query._yield_per:
|
||||
fetch = cursor.fetchmany(query._yield_per)
|
||||
if not fetch:
|
||||
break
|
||||
else:
|
||||
fetch = cursor.fetchall()
|
||||
|
||||
if custom_rows:
|
||||
rows = []
|
||||
for row in fetch:
|
||||
process[0](row, rows)
|
||||
elif single_entity:
|
||||
rows = [process[0](row, None) for row in fetch]
|
||||
else:
|
||||
rows = [util.KeyedTuple([proc(row, None) for proc in process],
|
||||
labels) for row in fetch]
|
||||
|
||||
if filtered:
|
||||
rows = util.unique_list(rows, filter_fn)
|
||||
|
||||
if context.refresh_state and query._only_load_props \
|
||||
and context.refresh_state in context.progress:
|
||||
context.refresh_state._commit(
|
||||
context.refresh_state.dict, query._only_load_props)
|
||||
context.progress.pop(context.refresh_state)
|
||||
|
||||
statelib.InstanceState._commit_all_states(
|
||||
list(context.progress.items()),
|
||||
session.identity_map
|
||||
)
|
||||
|
||||
for state, (dict_, attrs) in context.partials.items():
|
||||
state._commit(dict_, attrs)
|
||||
|
||||
for row in rows:
|
||||
yield row
|
||||
|
||||
if not query._yield_per:
|
||||
break
|
||||
|
||||
|
||||
@util.dependencies("sqlalchemy.orm.query")
|
||||
def merge_result(querylib, query, iterator, load=True):
|
||||
"""Merge a result into this :class:`.Query` object's Session."""
|
||||
|
||||
session = query.session
|
||||
if load:
|
||||
# flush current contents if we expect to load data
|
||||
session._autoflush()
|
||||
|
||||
autoflush = session.autoflush
|
||||
try:
|
||||
session.autoflush = False
|
||||
single_entity = len(query._entities) == 1
|
||||
if single_entity:
|
||||
if isinstance(query._entities[0], querylib._MapperEntity):
|
||||
result = [session._merge(
|
||||
attributes.instance_state(instance),
|
||||
attributes.instance_dict(instance),
|
||||
load=load, _recursive={})
|
||||
for instance in iterator]
|
||||
else:
|
||||
result = list(iterator)
|
||||
else:
|
||||
mapped_entities = [i for i, e in enumerate(query._entities)
|
||||
if isinstance(e, querylib._MapperEntity)]
|
||||
result = []
|
||||
keys = [ent._label_name for ent in query._entities]
|
||||
for row in iterator:
|
||||
newrow = list(row)
|
||||
for i in mapped_entities:
|
||||
if newrow[i] is not None:
|
||||
newrow[i] = session._merge(
|
||||
attributes.instance_state(newrow[i]),
|
||||
attributes.instance_dict(newrow[i]),
|
||||
load=load, _recursive={})
|
||||
result.append(util.KeyedTuple(newrow, keys))
|
||||
|
||||
return iter(result)
|
||||
finally:
|
||||
session.autoflush = autoflush
|
||||
|
||||
|
||||
def get_from_identity(session, key, passive):
|
||||
"""Look up the given key in the given session's identity map,
|
||||
check the object for expired state if found.
|
||||
|
||||
"""
|
||||
instance = session.identity_map.get(key)
|
||||
if instance is not None:
|
||||
|
||||
state = attributes.instance_state(instance)
|
||||
|
||||
# expired - ensure it still exists
|
||||
if state.expired:
|
||||
if not passive & attributes.SQL_OK:
|
||||
# TODO: no coverage here
|
||||
return attributes.PASSIVE_NO_RESULT
|
||||
elif not passive & attributes.RELATED_OBJECT_OK:
|
||||
# this mode is used within a flush and the instance's
|
||||
# expired state will be checked soon enough, if necessary
|
||||
return instance
|
||||
try:
|
||||
state(state, passive)
|
||||
except orm_exc.ObjectDeletedError:
|
||||
session._remove_newly_deleted([state])
|
||||
return None
|
||||
return instance
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def load_on_ident(query, key,
|
||||
refresh_state=None, lockmode=None,
|
||||
only_load_props=None):
|
||||
"""Load the given identity key from the database."""
|
||||
|
||||
if key is not None:
|
||||
ident = key[1]
|
||||
else:
|
||||
ident = None
|
||||
|
||||
if refresh_state is None:
|
||||
q = query._clone()
|
||||
q._get_condition()
|
||||
else:
|
||||
q = query._clone()
|
||||
|
||||
if ident is not None:
|
||||
mapper = query._mapper_zero()
|
||||
|
||||
(_get_clause, _get_params) = mapper._get_clause
|
||||
|
||||
# None present in ident - turn those comparisons
|
||||
# into "IS NULL"
|
||||
if None in ident:
|
||||
nones = set([
|
||||
_get_params[col].key for col, value in
|
||||
zip(mapper.primary_key, ident) if value is None
|
||||
])
|
||||
_get_clause = sql_util.adapt_criterion_to_null(
|
||||
_get_clause, nones)
|
||||
|
||||
_get_clause = q._adapt_clause(_get_clause, True, False)
|
||||
q._criterion = _get_clause
|
||||
|
||||
params = dict([
|
||||
(_get_params[primary_key].key, id_val)
|
||||
for id_val, primary_key in zip(ident, mapper.primary_key)
|
||||
])
|
||||
|
||||
q._params = params
|
||||
|
||||
if lockmode is not None:
|
||||
version_check = True
|
||||
q = q.with_lockmode(lockmode)
|
||||
elif query._for_update_arg is not None:
|
||||
version_check = True
|
||||
q._for_update_arg = query._for_update_arg
|
||||
else:
|
||||
version_check = False
|
||||
|
||||
q._get_options(
|
||||
populate_existing=bool(refresh_state),
|
||||
version_check=version_check,
|
||||
only_load_props=only_load_props,
|
||||
refresh_state=refresh_state)
|
||||
q._order_by = None
|
||||
|
||||
try:
|
||||
return q.one()
|
||||
except orm_exc.NoResultFound:
|
||||
return None
|
||||
|
||||
|
||||
def instance_processor(mapper, context, path, adapter,
|
||||
polymorphic_from=None,
|
||||
only_load_props=None,
|
||||
refresh_state=None,
|
||||
polymorphic_discriminator=None):
|
||||
|
||||
"""Produce a mapper level row processor callable
|
||||
which processes rows into mapped instances."""
|
||||
|
||||
# note that this method, most of which exists in a closure
|
||||
# called _instance(), resists being broken out, as
|
||||
# attempts to do so tend to add significant function
|
||||
# call overhead. _instance() is the most
|
||||
# performance-critical section in the whole ORM.
|
||||
|
||||
pk_cols = mapper.primary_key
|
||||
|
||||
if polymorphic_from or refresh_state:
|
||||
polymorphic_on = None
|
||||
else:
|
||||
if polymorphic_discriminator is not None:
|
||||
polymorphic_on = polymorphic_discriminator
|
||||
else:
|
||||
polymorphic_on = mapper.polymorphic_on
|
||||
polymorphic_instances = util.PopulateDict(
|
||||
_configure_subclass_mapper(
|
||||
mapper,
|
||||
context, path, adapter)
|
||||
)
|
||||
|
||||
version_id_col = mapper.version_id_col
|
||||
|
||||
if adapter:
|
||||
pk_cols = [adapter.columns[c] for c in pk_cols]
|
||||
if polymorphic_on is not None:
|
||||
polymorphic_on = adapter.columns[polymorphic_on]
|
||||
if version_id_col is not None:
|
||||
version_id_col = adapter.columns[version_id_col]
|
||||
|
||||
identity_class = mapper._identity_class
|
||||
|
||||
new_populators = []
|
||||
existing_populators = []
|
||||
eager_populators = []
|
||||
|
||||
load_path = context.query._current_path + path \
|
||||
if context.query._current_path.path \
|
||||
else path
|
||||
|
||||
def populate_state(state, dict_, row, isnew, only_load_props):
|
||||
if isnew:
|
||||
if context.propagate_options:
|
||||
state.load_options = context.propagate_options
|
||||
if state.load_options:
|
||||
state.load_path = load_path
|
||||
|
||||
if not new_populators:
|
||||
_populators(mapper, context, path, row, adapter,
|
||||
new_populators,
|
||||
existing_populators,
|
||||
eager_populators
|
||||
)
|
||||
|
||||
if isnew:
|
||||
populators = new_populators
|
||||
else:
|
||||
populators = existing_populators
|
||||
|
||||
if only_load_props is None:
|
||||
for key, populator in populators:
|
||||
populator(state, dict_, row)
|
||||
elif only_load_props:
|
||||
for key, populator in populators:
|
||||
if key in only_load_props:
|
||||
populator(state, dict_, row)
|
||||
|
||||
session_identity_map = context.session.identity_map
|
||||
|
||||
listeners = mapper.dispatch
|
||||
|
||||
translate_row = listeners.translate_row or None
|
||||
create_instance = listeners.create_instance or None
|
||||
populate_instance = listeners.populate_instance or None
|
||||
append_result = listeners.append_result or None
|
||||
populate_existing = context.populate_existing or mapper.always_refresh
|
||||
invoke_all_eagers = context.invoke_all_eagers
|
||||
|
||||
if mapper.allow_partial_pks:
|
||||
is_not_primary_key = _none_set.issuperset
|
||||
else:
|
||||
is_not_primary_key = _none_set.issubset
|
||||
|
||||
def _instance(row, result):
|
||||
if not new_populators and invoke_all_eagers:
|
||||
_populators(mapper, context, path, row, adapter,
|
||||
new_populators,
|
||||
existing_populators,
|
||||
eager_populators
|
||||
)
|
||||
|
||||
if translate_row:
|
||||
for fn in translate_row:
|
||||
ret = fn(mapper, context, row)
|
||||
if ret is not EXT_CONTINUE:
|
||||
row = ret
|
||||
break
|
||||
|
||||
if polymorphic_on is not None:
|
||||
discriminator = row[polymorphic_on]
|
||||
if discriminator is not None:
|
||||
_instance = polymorphic_instances[discriminator]
|
||||
if _instance:
|
||||
return _instance(row, result)
|
||||
|
||||
# determine identity key
|
||||
if refresh_state:
|
||||
identitykey = refresh_state.key
|
||||
if identitykey is None:
|
||||
# super-rare condition; a refresh is being called
|
||||
# on a non-instance-key instance; this is meant to only
|
||||
# occur within a flush()
|
||||
identitykey = mapper._identity_key_from_state(refresh_state)
|
||||
else:
|
||||
identitykey = (
|
||||
identity_class,
|
||||
tuple([row[column] for column in pk_cols])
|
||||
)
|
||||
|
||||
instance = session_identity_map.get(identitykey)
|
||||
if instance is not None:
|
||||
state = attributes.instance_state(instance)
|
||||
dict_ = attributes.instance_dict(instance)
|
||||
|
||||
isnew = state.runid != context.runid
|
||||
currentload = not isnew
|
||||
loaded_instance = False
|
||||
|
||||
if not currentload and \
|
||||
version_id_col is not None and \
|
||||
context.version_check and \
|
||||
mapper._get_state_attr_by_column(
|
||||
state,
|
||||
dict_,
|
||||
mapper.version_id_col) != \
|
||||
row[version_id_col]:
|
||||
|
||||
raise orm_exc.StaleDataError(
|
||||
"Instance '%s' has version id '%s' which "
|
||||
"does not match database-loaded version id '%s'."
|
||||
% (state_str(state),
|
||||
mapper._get_state_attr_by_column(
|
||||
state, dict_,
|
||||
mapper.version_id_col),
|
||||
row[version_id_col]))
|
||||
elif refresh_state:
|
||||
# out of band refresh_state detected (i.e. its not in the
|
||||
# session.identity_map) honor it anyway. this can happen
|
||||
# if a _get() occurs within save_obj(), such as
|
||||
# when eager_defaults is True.
|
||||
state = refresh_state
|
||||
instance = state.obj()
|
||||
dict_ = attributes.instance_dict(instance)
|
||||
isnew = state.runid != context.runid
|
||||
currentload = True
|
||||
loaded_instance = False
|
||||
else:
|
||||
# check for non-NULL values in the primary key columns,
|
||||
# else no entity is returned for the row
|
||||
if is_not_primary_key(identitykey[1]):
|
||||
return None
|
||||
|
||||
isnew = True
|
||||
currentload = True
|
||||
loaded_instance = True
|
||||
|
||||
if create_instance:
|
||||
for fn in create_instance:
|
||||
instance = fn(mapper, context,
|
||||
row, mapper.class_)
|
||||
if instance is not EXT_CONTINUE:
|
||||
manager = attributes.manager_of_class(
|
||||
instance.__class__)
|
||||
# TODO: if manager is None, raise a friendly error
|
||||
# about returning instances of unmapped types
|
||||
manager.setup_instance(instance)
|
||||
break
|
||||
else:
|
||||
instance = mapper.class_manager.new_instance()
|
||||
else:
|
||||
instance = mapper.class_manager.new_instance()
|
||||
|
||||
dict_ = attributes.instance_dict(instance)
|
||||
state = attributes.instance_state(instance)
|
||||
state.key = identitykey
|
||||
|
||||
# attach instance to session.
|
||||
state.session_id = context.session.hash_key
|
||||
session_identity_map.add(state)
|
||||
|
||||
if currentload or populate_existing:
|
||||
# state is being fully loaded, so populate.
|
||||
# add to the "context.progress" collection.
|
||||
if isnew:
|
||||
state.runid = context.runid
|
||||
context.progress[state] = dict_
|
||||
|
||||
if populate_instance:
|
||||
for fn in populate_instance:
|
||||
ret = fn(mapper, context, row, state,
|
||||
only_load_props=only_load_props,
|
||||
instancekey=identitykey, isnew=isnew)
|
||||
if ret is not EXT_CONTINUE:
|
||||
break
|
||||
else:
|
||||
populate_state(state, dict_, row, isnew, only_load_props)
|
||||
else:
|
||||
populate_state(state, dict_, row, isnew, only_load_props)
|
||||
|
||||
if loaded_instance:
|
||||
state.manager.dispatch.load(state, context)
|
||||
elif isnew:
|
||||
state.manager.dispatch.refresh(state, context, only_load_props)
|
||||
|
||||
elif state in context.partials or state.unloaded or eager_populators:
|
||||
# state is having a partial set of its attributes
|
||||
# refreshed. Populate those attributes,
|
||||
# and add to the "context.partials" collection.
|
||||
if state in context.partials:
|
||||
isnew = False
|
||||
(d_, attrs) = context.partials[state]
|
||||
else:
|
||||
isnew = True
|
||||
attrs = state.unloaded
|
||||
context.partials[state] = (dict_, attrs)
|
||||
|
||||
if populate_instance:
|
||||
for fn in populate_instance:
|
||||
ret = fn(mapper, context, row, state,
|
||||
only_load_props=attrs,
|
||||
instancekey=identitykey, isnew=isnew)
|
||||
if ret is not EXT_CONTINUE:
|
||||
break
|
||||
else:
|
||||
populate_state(state, dict_, row, isnew, attrs)
|
||||
else:
|
||||
populate_state(state, dict_, row, isnew, attrs)
|
||||
|
||||
for key, pop in eager_populators:
|
||||
if key not in state.unloaded:
|
||||
pop(state, dict_, row)
|
||||
|
||||
if isnew:
|
||||
state.manager.dispatch.refresh(state, context, attrs)
|
||||
|
||||
if result is not None:
|
||||
if append_result:
|
||||
for fn in append_result:
|
||||
if fn(mapper, context, row, state,
|
||||
result, instancekey=identitykey,
|
||||
isnew=isnew) is not EXT_CONTINUE:
|
||||
break
|
||||
else:
|
||||
result.append(instance)
|
||||
else:
|
||||
result.append(instance)
|
||||
|
||||
return instance
|
||||
return _instance
|
||||
|
||||
|
||||
def _populators(mapper, context, path, row, adapter,
|
||||
new_populators, existing_populators, eager_populators):
|
||||
"""Produce a collection of attribute level row processor
|
||||
callables."""
|
||||
|
||||
delayed_populators = []
|
||||
pops = (new_populators, existing_populators, delayed_populators,
|
||||
eager_populators)
|
||||
|
||||
for prop in mapper._props.values():
|
||||
|
||||
for i, pop in enumerate(prop.create_row_processor(
|
||||
context,
|
||||
path,
|
||||
mapper, row, adapter)):
|
||||
if pop is not None:
|
||||
pops[i].append((prop.key, pop))
|
||||
|
||||
if delayed_populators:
|
||||
new_populators.extend(delayed_populators)
|
||||
|
||||
|
||||
def _configure_subclass_mapper(mapper, context, path, adapter):
|
||||
"""Produce a mapper level row processor callable factory for mappers
|
||||
inheriting this one."""
|
||||
|
||||
def configure_subclass_mapper(discriminator):
|
||||
try:
|
||||
sub_mapper = mapper.polymorphic_map[discriminator]
|
||||
except KeyError:
|
||||
raise AssertionError(
|
||||
"No such polymorphic_identity %r is defined" %
|
||||
discriminator)
|
||||
if sub_mapper is mapper:
|
||||
return None
|
||||
|
||||
return instance_processor(
|
||||
sub_mapper,
|
||||
context,
|
||||
path,
|
||||
adapter,
|
||||
polymorphic_from=mapper)
|
||||
return configure_subclass_mapper
|
||||
|
||||
|
||||
def load_scalar_attributes(mapper, state, attribute_names):
|
||||
"""initiate a column-based attribute refresh operation."""
|
||||
|
||||
#assert mapper is _state_mapper(state)
|
||||
session = state.session
|
||||
if not session:
|
||||
raise orm_exc.DetachedInstanceError(
|
||||
"Instance %s is not bound to a Session; "
|
||||
"attribute refresh operation cannot proceed" %
|
||||
(state_str(state)))
|
||||
|
||||
has_key = bool(state.key)
|
||||
|
||||
result = False
|
||||
|
||||
if mapper.inherits and not mapper.concrete:
|
||||
statement = mapper._optimized_get_statement(state, attribute_names)
|
||||
if statement is not None:
|
||||
result = load_on_ident(
|
||||
session.query(mapper).from_statement(statement),
|
||||
None,
|
||||
only_load_props=attribute_names,
|
||||
refresh_state=state
|
||||
)
|
||||
|
||||
if result is False:
|
||||
if has_key:
|
||||
identity_key = state.key
|
||||
else:
|
||||
# this codepath is rare - only valid when inside a flush, and the
|
||||
# object is becoming persistent but hasn't yet been assigned
|
||||
# an identity_key.
|
||||
# check here to ensure we have the attrs we need.
|
||||
pk_attrs = [mapper._columntoproperty[col].key
|
||||
for col in mapper.primary_key]
|
||||
if state.expired_attributes.intersection(pk_attrs):
|
||||
raise sa_exc.InvalidRequestError(
|
||||
"Instance %s cannot be refreshed - it's not "
|
||||
" persistent and does not "
|
||||
"contain a full primary key." % state_str(state))
|
||||
identity_key = mapper._identity_key_from_state(state)
|
||||
|
||||
if (_none_set.issubset(identity_key) and \
|
||||
not mapper.allow_partial_pks) or \
|
||||
_none_set.issuperset(identity_key):
|
||||
util.warn("Instance %s to be refreshed doesn't "
|
||||
"contain a full primary key - can't be refreshed "
|
||||
"(and shouldn't be expired, either)."
|
||||
% state_str(state))
|
||||
return
|
||||
|
||||
result = load_on_ident(
|
||||
session.query(mapper),
|
||||
identity_key,
|
||||
refresh_state=state,
|
||||
only_load_props=attribute_names)
|
||||
|
||||
# if instance is pending, a refresh operation
|
||||
# may not complete (even if PK attributes are assigned)
|
||||
if has_key and result is None:
|
||||
raise orm_exc.ObjectDeletedError(state)
|
||||
+997
-697
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,261 @@
|
||||
# orm/path_registry.py
|
||||
# Copyright (C) 2005-2014 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
|
||||
"""Path tracking utilities, representing mapper graph traversals.
|
||||
|
||||
"""
|
||||
|
||||
from .. import inspection
|
||||
from .. import util
|
||||
from .. import exc
|
||||
from itertools import chain
|
||||
from .base import class_mapper
|
||||
|
||||
def _unreduce_path(path):
|
||||
return PathRegistry.deserialize(path)
|
||||
|
||||
|
||||
_WILDCARD_TOKEN = "*"
|
||||
_DEFAULT_TOKEN = "_sa_default"
|
||||
|
||||
class PathRegistry(object):
|
||||
"""Represent query load paths and registry functions.
|
||||
|
||||
Basically represents structures like:
|
||||
|
||||
(<User mapper>, "orders", <Order mapper>, "items", <Item mapper>)
|
||||
|
||||
These structures are generated by things like
|
||||
query options (joinedload(), subqueryload(), etc.) and are
|
||||
used to compose keys stored in the query._attributes dictionary
|
||||
for various options.
|
||||
|
||||
They are then re-composed at query compile/result row time as
|
||||
the query is formed and as rows are fetched, where they again
|
||||
serve to compose keys to look up options in the context.attributes
|
||||
dictionary, which is copied from query._attributes.
|
||||
|
||||
The path structure has a limited amount of caching, where each
|
||||
"root" ultimately pulls from a fixed registry associated with
|
||||
the first mapper, that also contains elements for each of its
|
||||
property keys. However paths longer than two elements, which
|
||||
are the exception rather than the rule, are generated on an
|
||||
as-needed basis.
|
||||
|
||||
"""
|
||||
|
||||
def __eq__(self, other):
|
||||
return other is not None and \
|
||||
self.path == other.path
|
||||
|
||||
def set(self, attributes, key, value):
|
||||
attributes[(key, self.path)] = value
|
||||
|
||||
def setdefault(self, attributes, key, value):
|
||||
attributes.setdefault((key, self.path), value)
|
||||
|
||||
def get(self, attributes, key, value=None):
|
||||
key = (key, self.path)
|
||||
if key in attributes:
|
||||
return attributes[key]
|
||||
else:
|
||||
return value
|
||||
|
||||
def __len__(self):
|
||||
return len(self.path)
|
||||
|
||||
@property
|
||||
def length(self):
|
||||
return len(self.path)
|
||||
|
||||
def pairs(self):
|
||||
path = self.path
|
||||
for i in range(0, len(path), 2):
|
||||
yield path[i], path[i + 1]
|
||||
|
||||
def contains_mapper(self, mapper):
|
||||
for path_mapper in [
|
||||
self.path[i] for i in range(0, len(self.path), 2)
|
||||
]:
|
||||
if path_mapper.is_mapper and \
|
||||
path_mapper.isa(mapper):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def contains(self, attributes, key):
|
||||
return (key, self.path) in attributes
|
||||
|
||||
def __reduce__(self):
|
||||
return _unreduce_path, (self.serialize(), )
|
||||
|
||||
def serialize(self):
|
||||
path = self.path
|
||||
return list(zip(
|
||||
[m.class_ for m in [path[i] for i in range(0, len(path), 2)]],
|
||||
[path[i].key for i in range(1, len(path), 2)] + [None]
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def deserialize(cls, path):
|
||||
if path is None:
|
||||
return None
|
||||
|
||||
p = tuple(chain(*[(class_mapper(mcls),
|
||||
class_mapper(mcls).attrs[key]
|
||||
if key is not None else None)
|
||||
for mcls, key in path]))
|
||||
if p and p[-1] is None:
|
||||
p = p[0:-1]
|
||||
return cls.coerce(p)
|
||||
|
||||
@classmethod
|
||||
def per_mapper(cls, mapper):
|
||||
return EntityRegistry(
|
||||
cls.root, mapper
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def coerce(cls, raw):
|
||||
return util.reduce(lambda prev, next: prev[next], raw, cls.root)
|
||||
|
||||
def token(self, token):
|
||||
if token.endswith(':' + _WILDCARD_TOKEN):
|
||||
return TokenRegistry(self, token)
|
||||
elif token.endswith(":" + _DEFAULT_TOKEN):
|
||||
return TokenRegistry(self.root, token)
|
||||
else:
|
||||
raise exc.ArgumentError("invalid token: %s" % token)
|
||||
|
||||
def __add__(self, other):
|
||||
return util.reduce(
|
||||
lambda prev, next: prev[next],
|
||||
other.path, self)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%r)" % (self.__class__.__name__, self.path, )
|
||||
|
||||
|
||||
class RootRegistry(PathRegistry):
|
||||
"""Root registry, defers to mappers so that
|
||||
paths are maintained per-root-mapper.
|
||||
|
||||
"""
|
||||
path = ()
|
||||
has_entity = False
|
||||
def __getitem__(self, entity):
|
||||
return entity._path_registry
|
||||
|
||||
PathRegistry.root = RootRegistry()
|
||||
|
||||
class TokenRegistry(PathRegistry):
|
||||
def __init__(self, parent, token):
|
||||
self.token = token
|
||||
self.parent = parent
|
||||
self.path = parent.path + (token,)
|
||||
|
||||
has_entity = False
|
||||
|
||||
def __getitem__(self, entity):
|
||||
raise NotImplementedError()
|
||||
|
||||
class PropRegistry(PathRegistry):
|
||||
def __init__(self, parent, prop):
|
||||
# restate this path in terms of the
|
||||
# given MapperProperty's parent.
|
||||
insp = inspection.inspect(parent[-1])
|
||||
if not insp.is_aliased_class or insp._use_mapper_path:
|
||||
parent = parent.parent[prop.parent]
|
||||
elif insp.is_aliased_class and insp.with_polymorphic_mappers:
|
||||
if prop.parent is not insp.mapper and \
|
||||
prop.parent in insp.with_polymorphic_mappers:
|
||||
subclass_entity = parent[-1]._entity_for_mapper(prop.parent)
|
||||
parent = parent.parent[subclass_entity]
|
||||
|
||||
self.prop = prop
|
||||
self.parent = parent
|
||||
self.path = parent.path + (prop,)
|
||||
|
||||
@util.memoized_property
|
||||
def has_entity(self):
|
||||
return hasattr(self.prop, "mapper")
|
||||
|
||||
@util.memoized_property
|
||||
def entity(self):
|
||||
return self.prop.mapper
|
||||
|
||||
@util.memoized_property
|
||||
def _wildcard_path_loader_key(self):
|
||||
"""Given a path (mapper A, prop X), replace the prop with the wildcard,
|
||||
e.g. (mapper A, 'relationship:.*') or (mapper A, 'column:.*'), then
|
||||
return within the ("loader", path) structure.
|
||||
|
||||
"""
|
||||
return ("loader",
|
||||
self.parent.token(
|
||||
"%s:%s" % (self.prop.strategy_wildcard_key, _WILDCARD_TOKEN)
|
||||
).path
|
||||
)
|
||||
|
||||
@util.memoized_property
|
||||
def _default_path_loader_key(self):
|
||||
return ("loader",
|
||||
self.parent.token(
|
||||
"%s:%s" % (self.prop.strategy_wildcard_key, _DEFAULT_TOKEN)
|
||||
).path
|
||||
)
|
||||
|
||||
@util.memoized_property
|
||||
def _loader_key(self):
|
||||
return ("loader", self.path)
|
||||
|
||||
@property
|
||||
def mapper(self):
|
||||
return self.entity
|
||||
|
||||
@property
|
||||
def entity_path(self):
|
||||
return self[self.entity]
|
||||
|
||||
def __getitem__(self, entity):
|
||||
if isinstance(entity, (int, slice)):
|
||||
return self.path[entity]
|
||||
else:
|
||||
return EntityRegistry(
|
||||
self, entity
|
||||
)
|
||||
|
||||
class EntityRegistry(PathRegistry, dict):
|
||||
is_aliased_class = False
|
||||
has_entity = True
|
||||
|
||||
def __init__(self, parent, entity):
|
||||
self.key = entity
|
||||
self.parent = parent
|
||||
self.is_aliased_class = entity.is_aliased_class
|
||||
self.entity = entity
|
||||
self.path = parent.path + (entity,)
|
||||
self.entity_path = self
|
||||
|
||||
@property
|
||||
def mapper(self):
|
||||
return inspection.inspect(self.entity).mapper
|
||||
|
||||
def __bool__(self):
|
||||
return True
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def __getitem__(self, entity):
|
||||
if isinstance(entity, (int, slice)):
|
||||
return self.path[entity]
|
||||
else:
|
||||
return dict.__getitem__(self, entity)
|
||||
|
||||
def __missing__(self, key):
|
||||
self[key] = item = PropRegistry(self, key)
|
||||
return item
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# orm/persistence.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 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
|
||||
@@ -15,12 +15,12 @@ in unitofwork.py.
|
||||
|
||||
import operator
|
||||
from itertools import groupby
|
||||
from .. import sql, util, exc as sa_exc, schema
|
||||
from . import attributes, sync, exc as orm_exc, evaluator
|
||||
from .base import _state_mapper, state_str, _attr_as_key
|
||||
from ..sql import expression
|
||||
from . import loading
|
||||
|
||||
from sqlalchemy import sql, util, exc as sa_exc
|
||||
from sqlalchemy.orm import attributes, sync, \
|
||||
exc as orm_exc
|
||||
|
||||
from sqlalchemy.orm.util import _state_mapper, state_str
|
||||
|
||||
def save_obj(base_mapper, states, uowtransaction, single=False):
|
||||
"""Issue ``INSERT`` and/or ``UPDATE`` statements for a list
|
||||
@@ -46,7 +46,7 @@ def save_obj(base_mapper, states, uowtransaction, single=False):
|
||||
|
||||
cached_connections = _cached_connection_dict(base_mapper)
|
||||
|
||||
for table, mapper in base_mapper._sorted_tables.iteritems():
|
||||
for table, mapper in base_mapper._sorted_tables.items():
|
||||
insert = _collect_insert_commands(base_mapper, uowtransaction,
|
||||
table, states_to_insert)
|
||||
|
||||
@@ -61,11 +61,12 @@ def save_obj(base_mapper, states, uowtransaction, single=False):
|
||||
if insert:
|
||||
_emit_insert_statements(base_mapper, uowtransaction,
|
||||
cached_connections,
|
||||
table, insert)
|
||||
mapper, table, insert)
|
||||
|
||||
_finalize_insert_update_commands(base_mapper, uowtransaction,
|
||||
states_to_insert, states_to_update)
|
||||
|
||||
|
||||
def post_update(base_mapper, states, uowtransaction, post_update_cols):
|
||||
"""Issue UPDATE statements on behalf of a relationship() which
|
||||
specifies post_update.
|
||||
@@ -77,8 +78,7 @@ def post_update(base_mapper, states, uowtransaction, post_update_cols):
|
||||
base_mapper,
|
||||
states, uowtransaction)
|
||||
|
||||
|
||||
for table, mapper in base_mapper._sorted_tables.iteritems():
|
||||
for table, mapper in base_mapper._sorted_tables.items():
|
||||
update = _collect_post_update_commands(base_mapper, uowtransaction,
|
||||
table, states_to_update,
|
||||
post_update_cols)
|
||||
@@ -88,6 +88,7 @@ def post_update(base_mapper, states, uowtransaction, post_update_cols):
|
||||
cached_connections,
|
||||
mapper, table, update)
|
||||
|
||||
|
||||
def delete_obj(base_mapper, states, uowtransaction):
|
||||
"""Issue ``DELETE`` statements for a list of objects.
|
||||
|
||||
@@ -105,7 +106,7 @@ def delete_obj(base_mapper, states, uowtransaction):
|
||||
|
||||
table_to_mapper = base_mapper._sorted_tables
|
||||
|
||||
for table in reversed(table_to_mapper.keys()):
|
||||
for table in reversed(list(table_to_mapper.keys())):
|
||||
delete = _collect_delete_commands(base_mapper, uowtransaction,
|
||||
table, states_to_delete)
|
||||
|
||||
@@ -118,6 +119,7 @@ def delete_obj(base_mapper, states, uowtransaction):
|
||||
in states_to_delete:
|
||||
mapper.dispatch.after_delete(mapper, connection, state)
|
||||
|
||||
|
||||
def _organize_states_for_save(base_mapper, states, uowtransaction):
|
||||
"""Make an initial pass across a set of states for INSERT or
|
||||
UPDATE.
|
||||
@@ -148,12 +150,15 @@ def _organize_states_for_save(base_mapper, states, uowtransaction):
|
||||
else:
|
||||
mapper.dispatch.before_update(mapper, connection, state)
|
||||
|
||||
if mapper._validate_polymorphic_identity:
|
||||
mapper._validate_polymorphic_identity(mapper, state, dict_)
|
||||
|
||||
# detect if we have a "pending" instance (i.e. has
|
||||
# no instance_key attached to it), and another instance
|
||||
# with the same identity key already exists as persistent.
|
||||
# convert to an UPDATE if so.
|
||||
if not has_identity and \
|
||||
instance_key in uowtransaction.session.identity_map:
|
||||
instance_key in uowtransaction.session.identity_map:
|
||||
instance = \
|
||||
uowtransaction.session.identity_map[instance_key]
|
||||
existing = attributes.instance_state(instance)
|
||||
@@ -187,6 +192,7 @@ def _organize_states_for_save(base_mapper, states, uowtransaction):
|
||||
|
||||
return states_to_insert, states_to_update
|
||||
|
||||
|
||||
def _organize_states_for_post_update(base_mapper, states,
|
||||
uowtransaction):
|
||||
"""Make an initial pass across a set of states for UPDATE
|
||||
@@ -200,6 +206,7 @@ def _organize_states_for_post_update(base_mapper, states,
|
||||
return list(_connections_for_states(base_mapper, uowtransaction,
|
||||
states))
|
||||
|
||||
|
||||
def _organize_states_for_delete(base_mapper, states, uowtransaction):
|
||||
"""Make an initial pass across a set of states for DELETE.
|
||||
|
||||
@@ -220,6 +227,7 @@ def _organize_states_for_delete(base_mapper, states, uowtransaction):
|
||||
bool(state.key), connection))
|
||||
return states_to_delete
|
||||
|
||||
|
||||
def _collect_insert_commands(base_mapper, uowtransaction, table,
|
||||
states_to_insert):
|
||||
"""Identify sets of values to use in INSERT statements for a
|
||||
@@ -238,9 +246,12 @@ def _collect_insert_commands(base_mapper, uowtransaction, table,
|
||||
value_params = {}
|
||||
|
||||
has_all_pks = True
|
||||
has_all_defaults = True
|
||||
for col in mapper._cols_by_table[table]:
|
||||
if col is mapper.version_id_col:
|
||||
params[col.key] = mapper.version_id_generator(None)
|
||||
if col is mapper.version_id_col and \
|
||||
mapper.version_id_generator is not False:
|
||||
val = mapper.version_id_generator(None)
|
||||
params[col.key] = val
|
||||
else:
|
||||
# pull straight from the dict for
|
||||
# pending objects
|
||||
@@ -253,6 +264,9 @@ def _collect_insert_commands(base_mapper, uowtransaction, table,
|
||||
elif col.default is None and \
|
||||
col.server_default is None:
|
||||
params[col.key] = value
|
||||
elif col.server_default is not None and \
|
||||
mapper.base_mapper.eager_defaults:
|
||||
has_all_defaults = False
|
||||
|
||||
elif isinstance(value, sql.ClauseElement):
|
||||
value_params[col] = value
|
||||
@@ -260,9 +274,11 @@ def _collect_insert_commands(base_mapper, uowtransaction, table,
|
||||
params[col.key] = value
|
||||
|
||||
insert.append((state, state_dict, params, mapper,
|
||||
connection, value_params, has_all_pks))
|
||||
connection, value_params, has_all_pks,
|
||||
has_all_defaults))
|
||||
return insert
|
||||
|
||||
|
||||
def _collect_update_commands(base_mapper, uowtransaction,
|
||||
table, states_to_update):
|
||||
"""Identify sets of values to use in UPDATE statements for a
|
||||
@@ -306,19 +322,20 @@ def _collect_update_commands(base_mapper, uowtransaction,
|
||||
params[col.key] = history.added[0]
|
||||
hasdata = True
|
||||
else:
|
||||
params[col.key] = mapper.version_id_generator(
|
||||
params[col._label])
|
||||
if mapper.version_id_generator is not False:
|
||||
val = mapper.version_id_generator(params[col._label])
|
||||
params[col.key] = val
|
||||
|
||||
# HACK: check for history, in case the
|
||||
# history is only
|
||||
# in a different table than the one
|
||||
# where the version_id_col is.
|
||||
for prop in mapper._columntoproperty.itervalues():
|
||||
history = attributes.get_state_history(
|
||||
state, prop.key,
|
||||
attributes.PASSIVE_NO_INITIALIZE)
|
||||
if history.added:
|
||||
hasdata = True
|
||||
# HACK: check for history, in case the
|
||||
# history is only
|
||||
# in a different table than the one
|
||||
# where the version_id_col is.
|
||||
for prop in mapper._columntoproperty.values():
|
||||
history = attributes.get_state_history(
|
||||
state, prop.key,
|
||||
attributes.PASSIVE_NO_INITIALIZE)
|
||||
if history.added:
|
||||
hasdata = True
|
||||
else:
|
||||
prop = mapper._columntoproperty[col]
|
||||
history = attributes.get_state_history(
|
||||
@@ -370,7 +387,7 @@ def _collect_update_commands(base_mapper, uowtransaction,
|
||||
params[col._label] = value
|
||||
if hasdata:
|
||||
if hasnull:
|
||||
raise sa_exc.FlushError(
|
||||
raise orm_exc.FlushError(
|
||||
"Can't update table "
|
||||
"using NULL for primary "
|
||||
"key value")
|
||||
@@ -400,6 +417,7 @@ def _collect_post_update_commands(base_mapper, uowtransaction, table,
|
||||
mapper._get_state_attr_by_column(
|
||||
state,
|
||||
state_dict, col)
|
||||
|
||||
elif col in post_update_cols:
|
||||
prop = mapper._columntoproperty[col]
|
||||
history = attributes.get_state_history(
|
||||
@@ -414,6 +432,7 @@ def _collect_post_update_commands(base_mapper, uowtransaction, table,
|
||||
connection))
|
||||
return update
|
||||
|
||||
|
||||
def _collect_delete_commands(base_mapper, uowtransaction, table,
|
||||
states_to_delete):
|
||||
"""Identify values to use in DELETE statements for a list of
|
||||
@@ -434,7 +453,7 @@ def _collect_delete_commands(base_mapper, uowtransaction, table,
|
||||
mapper._get_state_attr_by_column(
|
||||
state, state_dict, col)
|
||||
if value is None:
|
||||
raise sa_exc.FlushError(
|
||||
raise orm_exc.FlushError(
|
||||
"Can't delete from table "
|
||||
"using NULL for primary "
|
||||
"key value")
|
||||
@@ -468,7 +487,13 @@ def _emit_update_statements(base_mapper, uowtransaction,
|
||||
sql.bindparam(mapper.version_id_col._label,
|
||||
type_=mapper.version_id_col.type))
|
||||
|
||||
return table.update(clause)
|
||||
stmt = table.update(clause)
|
||||
if mapper.base_mapper.eager_defaults:
|
||||
stmt = stmt.return_defaults()
|
||||
elif mapper.version_id_col is not None:
|
||||
stmt = stmt.return_defaults(mapper.version_id_col)
|
||||
|
||||
return stmt
|
||||
|
||||
statement = base_mapper._memo(('update', table), update_stmt)
|
||||
|
||||
@@ -490,8 +515,7 @@ def _emit_update_statements(base_mapper, uowtransaction,
|
||||
table,
|
||||
state,
|
||||
state_dict,
|
||||
c.context.prefetch_cols,
|
||||
c.context.postfetch_cols,
|
||||
c,
|
||||
c.context.compiled_parameters[0],
|
||||
value_params)
|
||||
rows += c.rowcount
|
||||
@@ -509,45 +533,57 @@ def _emit_update_statements(base_mapper, uowtransaction,
|
||||
c.dialect.dialect_description,
|
||||
stacklevel=12)
|
||||
|
||||
|
||||
def _emit_insert_statements(base_mapper, uowtransaction,
|
||||
cached_connections, table, insert):
|
||||
cached_connections, mapper, table, insert):
|
||||
"""Emit INSERT statements corresponding to value lists collected
|
||||
by _collect_insert_commands()."""
|
||||
|
||||
statement = base_mapper._memo(('insert', table), table.insert)
|
||||
|
||||
for (connection, pkeys, hasvalue, has_all_pks), \
|
||||
for (connection, pkeys, hasvalue, has_all_pks, has_all_defaults), \
|
||||
records in groupby(insert,
|
||||
lambda rec: (rec[4],
|
||||
rec[2].keys(),
|
||||
list(rec[2].keys()),
|
||||
bool(rec[5]),
|
||||
rec[6])
|
||||
rec[6], rec[7])
|
||||
):
|
||||
if has_all_pks and not hasvalue:
|
||||
if \
|
||||
(
|
||||
has_all_defaults
|
||||
or not base_mapper.eager_defaults
|
||||
or not connection.dialect.implicit_returning
|
||||
) and has_all_pks and not hasvalue:
|
||||
|
||||
records = list(records)
|
||||
multiparams = [rec[2] for rec in records]
|
||||
|
||||
c = cached_connections[connection].\
|
||||
execute(statement, multiparams)
|
||||
|
||||
for (state, state_dict, params, mapper,
|
||||
conn, value_params, has_all_pks), \
|
||||
for (state, state_dict, params, mapper_rec,
|
||||
conn, value_params, has_all_pks, has_all_defaults), \
|
||||
last_inserted_params in \
|
||||
zip(records, c.context.compiled_parameters):
|
||||
_postfetch(
|
||||
mapper,
|
||||
mapper_rec,
|
||||
uowtransaction,
|
||||
table,
|
||||
state,
|
||||
state_dict,
|
||||
c.context.prefetch_cols,
|
||||
c.context.postfetch_cols,
|
||||
c,
|
||||
last_inserted_params,
|
||||
value_params)
|
||||
|
||||
else:
|
||||
for state, state_dict, params, mapper, \
|
||||
if not has_all_defaults and base_mapper.eager_defaults:
|
||||
statement = statement.return_defaults()
|
||||
elif mapper.version_id_col is not None:
|
||||
statement = statement.return_defaults(mapper.version_id_col)
|
||||
|
||||
for state, state_dict, params, mapper_rec, \
|
||||
connection, value_params, \
|
||||
has_all_pks in records:
|
||||
has_all_pks, has_all_defaults in records:
|
||||
|
||||
if value_params:
|
||||
result = connection.execute(
|
||||
@@ -563,28 +599,26 @@ def _emit_insert_statements(base_mapper, uowtransaction,
|
||||
# set primary key attributes
|
||||
for pk, col in zip(primary_key,
|
||||
mapper._pks_by_table[table]):
|
||||
prop = mapper._columntoproperty[col]
|
||||
prop = mapper_rec._columntoproperty[col]
|
||||
if state_dict.get(prop.key) is None:
|
||||
# TODO: would rather say:
|
||||
#state_dict[prop.key] = pk
|
||||
mapper._set_state_attr_by_column(
|
||||
mapper_rec._set_state_attr_by_column(
|
||||
state,
|
||||
state_dict,
|
||||
col, pk)
|
||||
|
||||
_postfetch(
|
||||
mapper,
|
||||
mapper_rec,
|
||||
uowtransaction,
|
||||
table,
|
||||
state,
|
||||
state_dict,
|
||||
result.context.prefetch_cols,
|
||||
result.context.postfetch_cols,
|
||||
result,
|
||||
result.context.compiled_parameters[0],
|
||||
value_params)
|
||||
|
||||
|
||||
|
||||
def _emit_post_update_statements(base_mapper, uowtransaction,
|
||||
cached_connections, mapper, table, update):
|
||||
"""Emit UPDATE statements corresponding to value lists collected
|
||||
@@ -606,7 +640,7 @@ def _emit_post_update_statements(base_mapper, uowtransaction,
|
||||
# also group them into common (connection, cols) sets
|
||||
# to support executemany().
|
||||
for key, grouper in groupby(
|
||||
update, lambda rec: (rec[4], rec[2].keys())
|
||||
update, lambda rec: (rec[4], list(rec[2].keys()))
|
||||
):
|
||||
connection = key[0]
|
||||
multiparams = [params for state, state_dict,
|
||||
@@ -640,7 +674,7 @@ def _emit_delete_statements(base_mapper, uowtransaction, cached_connections,
|
||||
|
||||
return table.delete(clause)
|
||||
|
||||
for connection, del_objects in delete.iteritems():
|
||||
for connection, del_objects in delete.items():
|
||||
statement = base_mapper._memo(('delete', table), delete_stmt)
|
||||
|
||||
connection = cached_connections[connection]
|
||||
@@ -687,15 +721,27 @@ def _finalize_insert_update_commands(base_mapper, uowtransaction,
|
||||
if p.expire_on_flush or p.key not in state.dict]
|
||||
)
|
||||
if readonly:
|
||||
state.expire_attributes(state.dict, readonly)
|
||||
state._expire_attributes(state.dict, readonly)
|
||||
|
||||
# if eager_defaults option is enabled,
|
||||
# refresh whatever has been expired.
|
||||
if base_mapper.eager_defaults and state.unloaded:
|
||||
# if eager_defaults option is enabled, load
|
||||
# all expired cols. Else if we have a version_id_col, make sure
|
||||
# it isn't expired.
|
||||
toload_now = []
|
||||
|
||||
if base_mapper.eager_defaults:
|
||||
toload_now.extend(state._unloaded_non_object)
|
||||
elif mapper.version_id_col is not None and \
|
||||
mapper.version_id_generator is False:
|
||||
prop = mapper._columntoproperty[mapper.version_id_col]
|
||||
if prop.key in state.unloaded:
|
||||
toload_now.extend([prop.key])
|
||||
|
||||
if toload_now:
|
||||
state.key = base_mapper._identity_key_from_state(state)
|
||||
uowtransaction.session.query(base_mapper)._load_on_ident(
|
||||
loading.load_on_ident(
|
||||
uowtransaction.session.query(base_mapper),
|
||||
state.key, refresh_state=state,
|
||||
only_load_props=state.unloaded)
|
||||
only_load_props=toload_now)
|
||||
|
||||
# call after_XXX extensions
|
||||
if not has_identity:
|
||||
@@ -703,22 +749,34 @@ def _finalize_insert_update_commands(base_mapper, uowtransaction,
|
||||
else:
|
||||
mapper.dispatch.after_update(mapper, connection, state)
|
||||
|
||||
|
||||
def _postfetch(mapper, uowtransaction, table,
|
||||
state, dict_, prefetch_cols, postfetch_cols,
|
||||
params, value_params):
|
||||
state, dict_, result, params, value_params):
|
||||
"""Expire attributes in need of newly persisted database state,
|
||||
after an INSERT or UPDATE statement has proceeded for that
|
||||
state."""
|
||||
|
||||
prefetch_cols = result.context.prefetch_cols
|
||||
postfetch_cols = result.context.postfetch_cols
|
||||
returning_cols = result.context.returning_cols
|
||||
|
||||
if mapper.version_id_col is not None:
|
||||
prefetch_cols = list(prefetch_cols) + [mapper.version_id_col]
|
||||
|
||||
if returning_cols:
|
||||
row = result.context.returned_defaults
|
||||
if row is not None:
|
||||
for col in returning_cols:
|
||||
if col.primary_key:
|
||||
continue
|
||||
mapper._set_state_attr_by_column(state, dict_, col, row[col])
|
||||
|
||||
for c in prefetch_cols:
|
||||
if c.key in params and c in mapper._columntoproperty:
|
||||
mapper._set_state_attr_by_column(state, dict_, c, params[c.key])
|
||||
|
||||
if postfetch_cols:
|
||||
state.expire_attributes(state.dict,
|
||||
state._expire_attributes(state.dict,
|
||||
[mapper._columntoproperty[c].key
|
||||
for c in postfetch_cols if c in
|
||||
mapper._columntoproperty]
|
||||
@@ -733,6 +791,7 @@ def _postfetch(mapper, uowtransaction, table,
|
||||
uowtransaction,
|
||||
mapper.passive_updates)
|
||||
|
||||
|
||||
def _connections_for_states(base_mapper, uowtransaction, states):
|
||||
"""Return an iterator of (state, state.dict, mapper, connection).
|
||||
|
||||
@@ -762,18 +821,265 @@ def _connections_for_states(base_mapper, uowtransaction, states):
|
||||
|
||||
yield state, state.dict, mapper, connection
|
||||
|
||||
|
||||
def _cached_connection_dict(base_mapper):
|
||||
# dictionary of connection->connection_with_cache_options.
|
||||
return util.PopulateDict(
|
||||
lambda conn:conn.execution_options(
|
||||
lambda conn: conn.execution_options(
|
||||
compiled_cache=base_mapper._compiled_cache
|
||||
))
|
||||
|
||||
|
||||
def _sort_states(states):
|
||||
pending = set(states)
|
||||
persistent = set(s for s in pending if s.key is not None)
|
||||
pending.difference_update(persistent)
|
||||
return sorted(pending, key=operator.attrgetter("insert_order")) + \
|
||||
sorted(persistent, key=lambda q:q.key[1])
|
||||
sorted(persistent, key=lambda q: q.key[1])
|
||||
|
||||
|
||||
class BulkUD(object):
|
||||
"""Handle bulk update and deletes via a :class:`.Query`."""
|
||||
|
||||
def __init__(self, query):
|
||||
self.query = query.enable_eagerloads(False)
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
return self.query.session
|
||||
|
||||
@classmethod
|
||||
def _factory(cls, lookup, synchronize_session, *arg):
|
||||
try:
|
||||
klass = lookup[synchronize_session]
|
||||
except KeyError:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Valid strategies for session synchronization "
|
||||
"are %s" % (", ".join(sorted(repr(x)
|
||||
for x in lookup))))
|
||||
else:
|
||||
return klass(*arg)
|
||||
|
||||
def exec_(self):
|
||||
self._do_pre()
|
||||
self._do_pre_synchronize()
|
||||
self._do_exec()
|
||||
self._do_post_synchronize()
|
||||
self._do_post()
|
||||
|
||||
def _do_pre(self):
|
||||
query = self.query
|
||||
self.context = context = query._compile_context()
|
||||
if len(context.statement.froms) != 1 or \
|
||||
not isinstance(context.statement.froms[0], schema.Table):
|
||||
|
||||
self.primary_table = query._only_entity_zero(
|
||||
"This operation requires only one Table or "
|
||||
"entity be specified as the target."
|
||||
).mapper.local_table
|
||||
else:
|
||||
self.primary_table = context.statement.froms[0]
|
||||
|
||||
session = query.session
|
||||
|
||||
if query._autoflush:
|
||||
session._autoflush()
|
||||
|
||||
def _do_pre_synchronize(self):
|
||||
pass
|
||||
|
||||
def _do_post_synchronize(self):
|
||||
pass
|
||||
|
||||
|
||||
class BulkEvaluate(BulkUD):
|
||||
"""BulkUD which does the 'evaluate' method of session state resolution."""
|
||||
|
||||
def _additional_evaluators(self, evaluator_compiler):
|
||||
pass
|
||||
|
||||
def _do_pre_synchronize(self):
|
||||
query = self.query
|
||||
try:
|
||||
evaluator_compiler = evaluator.EvaluatorCompiler()
|
||||
if query.whereclause is not None:
|
||||
eval_condition = evaluator_compiler.process(
|
||||
query.whereclause)
|
||||
else:
|
||||
def eval_condition(obj):
|
||||
return True
|
||||
|
||||
self._additional_evaluators(evaluator_compiler)
|
||||
|
||||
except evaluator.UnevaluatableError:
|
||||
raise sa_exc.InvalidRequestError(
|
||||
"Could not evaluate current criteria in Python. "
|
||||
"Specify 'fetch' or False for the "
|
||||
"synchronize_session parameter.")
|
||||
target_cls = query._mapper_zero().class_
|
||||
|
||||
#TODO: detect when the where clause is a trivial primary key match
|
||||
self.matched_objects = [
|
||||
obj for (cls, pk), obj in
|
||||
query.session.identity_map.items()
|
||||
if issubclass(cls, target_cls) and
|
||||
eval_condition(obj)]
|
||||
|
||||
|
||||
class BulkFetch(BulkUD):
|
||||
"""BulkUD which does the 'fetch' method of session state resolution."""
|
||||
|
||||
def _do_pre_synchronize(self):
|
||||
query = self.query
|
||||
session = query.session
|
||||
select_stmt = self.context.statement.with_only_columns(
|
||||
self.primary_table.primary_key)
|
||||
self.matched_rows = session.execute(
|
||||
select_stmt,
|
||||
params=query._params).fetchall()
|
||||
|
||||
|
||||
class BulkUpdate(BulkUD):
|
||||
"""BulkUD which handles UPDATEs."""
|
||||
|
||||
def __init__(self, query, values):
|
||||
super(BulkUpdate, self).__init__(query)
|
||||
self.query._no_select_modifiers("update")
|
||||
self.values = values
|
||||
|
||||
@classmethod
|
||||
def factory(cls, query, synchronize_session, values):
|
||||
return BulkUD._factory({
|
||||
"evaluate": BulkUpdateEvaluate,
|
||||
"fetch": BulkUpdateFetch,
|
||||
False: BulkUpdate
|
||||
}, synchronize_session, query, values)
|
||||
|
||||
def _do_exec(self):
|
||||
update_stmt = sql.update(self.primary_table,
|
||||
self.context.whereclause, self.values)
|
||||
|
||||
self.result = self.query.session.execute(
|
||||
update_stmt, params=self.query._params)
|
||||
self.rowcount = self.result.rowcount
|
||||
|
||||
def _do_post(self):
|
||||
session = self.query.session
|
||||
session.dispatch.after_bulk_update(self)
|
||||
|
||||
|
||||
class BulkDelete(BulkUD):
|
||||
"""BulkUD which handles DELETEs."""
|
||||
|
||||
def __init__(self, query):
|
||||
super(BulkDelete, self).__init__(query)
|
||||
self.query._no_select_modifiers("delete")
|
||||
|
||||
@classmethod
|
||||
def factory(cls, query, synchronize_session):
|
||||
return BulkUD._factory({
|
||||
"evaluate": BulkDeleteEvaluate,
|
||||
"fetch": BulkDeleteFetch,
|
||||
False: BulkDelete
|
||||
}, synchronize_session, query)
|
||||
|
||||
def _do_exec(self):
|
||||
delete_stmt = sql.delete(self.primary_table,
|
||||
self.context.whereclause)
|
||||
|
||||
self.result = self.query.session.execute(delete_stmt,
|
||||
params=self.query._params)
|
||||
self.rowcount = self.result.rowcount
|
||||
|
||||
def _do_post(self):
|
||||
session = self.query.session
|
||||
session.dispatch.after_bulk_delete(self)
|
||||
|
||||
|
||||
class BulkUpdateEvaluate(BulkEvaluate, BulkUpdate):
|
||||
"""BulkUD which handles UPDATEs using the "evaluate"
|
||||
method of session resolution."""
|
||||
|
||||
def _additional_evaluators(self, evaluator_compiler):
|
||||
self.value_evaluators = {}
|
||||
for key, value in self.values.items():
|
||||
key = _attr_as_key(key)
|
||||
self.value_evaluators[key] = evaluator_compiler.process(
|
||||
expression._literal_as_binds(value))
|
||||
|
||||
def _do_post_synchronize(self):
|
||||
session = self.query.session
|
||||
states = set()
|
||||
evaluated_keys = list(self.value_evaluators.keys())
|
||||
for obj in self.matched_objects:
|
||||
state, dict_ = attributes.instance_state(obj),\
|
||||
attributes.instance_dict(obj)
|
||||
|
||||
# only evaluate unmodified attributes
|
||||
to_evaluate = state.unmodified.intersection(
|
||||
evaluated_keys)
|
||||
for key in to_evaluate:
|
||||
dict_[key] = self.value_evaluators[key](obj)
|
||||
|
||||
state._commit(dict_, list(to_evaluate))
|
||||
|
||||
# expire attributes with pending changes
|
||||
# (there was no autoflush, so they are overwritten)
|
||||
state._expire_attributes(dict_,
|
||||
set(evaluated_keys).
|
||||
difference(to_evaluate))
|
||||
states.add(state)
|
||||
session._register_altered(states)
|
||||
|
||||
|
||||
class BulkDeleteEvaluate(BulkEvaluate, BulkDelete):
|
||||
"""BulkUD which handles DELETEs using the "evaluate"
|
||||
method of session resolution."""
|
||||
|
||||
def _do_post_synchronize(self):
|
||||
self.query.session._remove_newly_deleted(
|
||||
[attributes.instance_state(obj)
|
||||
for obj in self.matched_objects])
|
||||
|
||||
|
||||
class BulkUpdateFetch(BulkFetch, BulkUpdate):
|
||||
"""BulkUD which handles UPDATEs using the "fetch"
|
||||
method of session resolution."""
|
||||
|
||||
def _do_post_synchronize(self):
|
||||
session = self.query.session
|
||||
target_mapper = self.query._mapper_zero()
|
||||
|
||||
states = set([
|
||||
attributes.instance_state(session.identity_map[identity_key])
|
||||
for identity_key in [
|
||||
target_mapper.identity_key_from_primary_key(
|
||||
list(primary_key))
|
||||
for primary_key in self.matched_rows
|
||||
]
|
||||
if identity_key in session.identity_map
|
||||
])
|
||||
attrib = [_attr_as_key(k) for k in self.values]
|
||||
for state in states:
|
||||
session._expire_state(state, attrib)
|
||||
session._register_altered(states)
|
||||
|
||||
|
||||
class BulkDeleteFetch(BulkFetch, BulkDelete):
|
||||
"""BulkUD which handles DELETEs using the "fetch"
|
||||
method of session resolution."""
|
||||
|
||||
def _do_post_synchronize(self):
|
||||
session = self.query.session
|
||||
target_mapper = self.query._mapper_zero()
|
||||
for primary_key in self.matched_rows:
|
||||
# TODO: inline this and call remove_newly_deleted
|
||||
# once
|
||||
identity_key = target_mapper.identity_key_from_primary_key(
|
||||
list(primary_key))
|
||||
if identity_key in session.identity_map:
|
||||
session._remove_newly_deleted(
|
||||
[attributes.instance_state(
|
||||
session.identity_map[identity_key]
|
||||
)]
|
||||
)
|
||||
|
||||
+124
-1430
File diff suppressed because it is too large
Load Diff
+1092
-948
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,67 +1,98 @@
|
||||
# orm/scoping.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 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 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
|
||||
from .. import exc as sa_exc
|
||||
from ..util import ScopedRegistry, ThreadLocalRegistry, warn
|
||||
from . import class_mapper, exc as orm_exc
|
||||
from .session import Session
|
||||
|
||||
|
||||
__all__ = ['ScopedSession']
|
||||
__all__ = ['scoped_session']
|
||||
|
||||
|
||||
class ScopedSession(object):
|
||||
"""Provides thread-local management of Sessions.
|
||||
class scoped_session(object):
|
||||
"""Provides scoped management of :class:`.Session` objects.
|
||||
|
||||
Typical invocation is via the :func:`.scoped_session`
|
||||
function::
|
||||
|
||||
Session = scoped_session(sessionmaker())
|
||||
|
||||
The internal registry is accessible,
|
||||
and by default is an instance of :class:`.ThreadLocalRegistry`.
|
||||
|
||||
See also: :ref:`unitofwork_contextual`.
|
||||
See :ref:`unitofwork_contextual` for a tutorial.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, session_factory, scopefunc=None):
|
||||
"""Construct a new :class:`.scoped_session`.
|
||||
|
||||
:param session_factory: a factory to create new :class:`.Session`
|
||||
instances. This is usually, but not necessarily, an instance
|
||||
of :class:`.sessionmaker`.
|
||||
:param scopefunc: optional function which defines
|
||||
the current scope. If not passed, the :class:`.scoped_session`
|
||||
object assumes "thread-local" scope, and will use
|
||||
a Python ``threading.local()`` in order to maintain the current
|
||||
:class:`.Session`. If passed, the function should return
|
||||
a hashable token; this token will be used as the key in a
|
||||
dictionary in order to store and retrieve the current
|
||||
:class:`.Session`.
|
||||
|
||||
"""
|
||||
self.session_factory = session_factory
|
||||
if scopefunc:
|
||||
self.registry = ScopedRegistry(session_factory, scopefunc)
|
||||
else:
|
||||
self.registry = ThreadLocalRegistry(session_factory)
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
if kwargs:
|
||||
scope = kwargs.pop('scope', False)
|
||||
def __call__(self, **kw):
|
||||
"""Return the current :class:`.Session`, creating it
|
||||
using the session factory if not present.
|
||||
|
||||
:param \**kw: Keyword arguments will be passed to the
|
||||
session factory callable, if an existing :class:`.Session`
|
||||
is not present. If the :class:`.Session` is present and
|
||||
keyword arguments have been passed,
|
||||
:exc:`~sqlalchemy.exc.InvalidRequestError` is raised.
|
||||
|
||||
"""
|
||||
if kw:
|
||||
scope = kw.pop('scope', False)
|
||||
if scope is not None:
|
||||
if self.registry.has():
|
||||
raise sa_exc.InvalidRequestError(
|
||||
"Scoped session is already present; "
|
||||
"no new arguments may be specified.")
|
||||
else:
|
||||
sess = self.session_factory(**kwargs)
|
||||
sess = self.session_factory(**kw)
|
||||
self.registry.set(sess)
|
||||
return sess
|
||||
else:
|
||||
return self.session_factory(**kwargs)
|
||||
return self.session_factory(**kw)
|
||||
else:
|
||||
return self.registry()
|
||||
|
||||
def remove(self):
|
||||
"""Dispose of the current contextual session."""
|
||||
"""Dispose of the current :class:`.Session`, if present.
|
||||
|
||||
This will first call :meth:`.Session.close` method
|
||||
on the current :class:`.Session`, which releases any existing
|
||||
transactional/connection resources still being held; transactions
|
||||
specifically are rolled back. The :class:`.Session` is then
|
||||
discarded. Upon next usage within the same scope,
|
||||
the :class:`.scoped_session` will produce a new
|
||||
:class:`.Session` object.
|
||||
|
||||
"""
|
||||
|
||||
if self.registry.has():
|
||||
self.registry().close()
|
||||
self.registry.clear()
|
||||
|
||||
def configure(self, **kwargs):
|
||||
"""reconfigure the sessionmaker used by this ScopedSession."""
|
||||
"""reconfigure the :class:`.sessionmaker` used by this
|
||||
:class:`.scoped_session`.
|
||||
|
||||
See :meth:`.sessionmaker.configure`.
|
||||
|
||||
"""
|
||||
|
||||
if self.registry.has():
|
||||
warn('At least one scoped session is already present. '
|
||||
@@ -71,8 +102,8 @@ class ScopedSession(object):
|
||||
self.session_factory.configure(**kwargs)
|
||||
|
||||
def query_property(self, query_cls=None):
|
||||
"""return a class property which produces a `Query` object
|
||||
against the class when called.
|
||||
"""return a class property which produces a :class:`.Query` object
|
||||
against the class and the current :class:`.Session` when called.
|
||||
|
||||
e.g.::
|
||||
|
||||
@@ -109,27 +140,37 @@ class ScopedSession(object):
|
||||
return None
|
||||
return query()
|
||||
|
||||
ScopedSession = scoped_session
|
||||
"""Old name for backwards compatibility."""
|
||||
|
||||
|
||||
def instrument(name):
|
||||
def do(self, *args, **kwargs):
|
||||
return getattr(self.registry(), name)(*args, **kwargs)
|
||||
return do
|
||||
|
||||
for meth in Session.public_methods:
|
||||
setattr(ScopedSession, meth, instrument(meth))
|
||||
setattr(scoped_session, meth, instrument(meth))
|
||||
|
||||
|
||||
def makeprop(name):
|
||||
def set(self, attr):
|
||||
setattr(self.registry(), name, attr)
|
||||
|
||||
def get(self):
|
||||
return getattr(self.registry(), name)
|
||||
|
||||
return property(get, set)
|
||||
|
||||
for prop in ('bind', 'dirty', 'deleted', 'new', 'identity_map',
|
||||
'is_active', 'autoflush', 'no_autoflush'):
|
||||
setattr(ScopedSession, prop, makeprop(prop))
|
||||
'is_active', 'autoflush', 'no_autoflush', 'info'):
|
||||
setattr(scoped_session, prop, makeprop(prop))
|
||||
|
||||
|
||||
def clslevel(name):
|
||||
def do(cls, *args, **kwargs):
|
||||
return getattr(Session, name)(*args, **kwargs)
|
||||
return classmethod(do)
|
||||
for prop in ('close_all', 'object_session', 'identity_key'):
|
||||
setattr(ScopedSession, prop, clslevel(prop))
|
||||
|
||||
for prop in ('close_all', 'object_session', 'identity_key'):
|
||||
setattr(scoped_session, prop, clslevel(prop))
|
||||
|
||||
+738
-458
File diff suppressed because it is too large
Load Diff
@@ -1,15 +0,0 @@
|
||||
# orm/shard.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
from sqlalchemy import util
|
||||
|
||||
util.warn_deprecated(
|
||||
"Horizontal sharding is now importable via "
|
||||
"'import sqlalchemy.ext.horizontal_shard"
|
||||
)
|
||||
|
||||
from sqlalchemy.ext.horizontal_shard import *
|
||||
|
||||
+293
-243
@@ -1,5 +1,5 @@
|
||||
# orm/state.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 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
|
||||
@@ -11,33 +11,30 @@ defines a large part of the ORM's interactivity.
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy.util import EMPTY_SET
|
||||
import weakref
|
||||
from sqlalchemy import util
|
||||
from .. import util
|
||||
from . import exc as orm_exc, interfaces
|
||||
from .path_registry import PathRegistry
|
||||
from .base import PASSIVE_NO_RESULT, SQL_OK, NEVER_SET, ATTR_WAS_SET, \
|
||||
NO_VALUE, PASSIVE_NO_INITIALIZE, INIT_OK, PASSIVE_OFF
|
||||
from . import base
|
||||
|
||||
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
|
||||
|
||||
class InstanceState(object):
|
||||
class InstanceState(interfaces._InspectionAttr):
|
||||
"""tracks state information at the instance level."""
|
||||
|
||||
session_id = None
|
||||
key = None
|
||||
runid = None
|
||||
load_options = EMPTY_SET
|
||||
load_options = util.EMPTY_SET
|
||||
load_path = ()
|
||||
insert_order = None
|
||||
mutable_dict = None
|
||||
_strong_obj = None
|
||||
modified = False
|
||||
expired = False
|
||||
deleted = False
|
||||
_load_pending = False
|
||||
|
||||
is_instance = True
|
||||
|
||||
def __init__(self, obj, manager):
|
||||
self.class_ = obj.__class__
|
||||
@@ -46,23 +43,127 @@ class InstanceState(object):
|
||||
self.callables = {}
|
||||
self.committed_state = {}
|
||||
|
||||
@util.memoized_property
|
||||
def attrs(self):
|
||||
"""Return a namespace representing each attribute on
|
||||
the mapped object, including its current value
|
||||
and history.
|
||||
|
||||
The returned object is an instance of :class:`.AttributeState`.
|
||||
|
||||
"""
|
||||
return util.ImmutableProperties(
|
||||
dict(
|
||||
(key, AttributeState(self, key))
|
||||
for key in self.manager
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def transient(self):
|
||||
"""Return true if the object is transient."""
|
||||
return self.key is None and \
|
||||
not self._attached
|
||||
|
||||
@property
|
||||
def pending(self):
|
||||
"""Return true if the object is pending."""
|
||||
return self.key is None and \
|
||||
self._attached
|
||||
|
||||
@property
|
||||
def persistent(self):
|
||||
"""Return true if the object is persistent."""
|
||||
return self.key is not None and \
|
||||
self._attached
|
||||
|
||||
@property
|
||||
def detached(self):
|
||||
"""Return true if the object is detached."""
|
||||
return self.key is not None and \
|
||||
not self._attached
|
||||
|
||||
@property
|
||||
@util.dependencies("sqlalchemy.orm.session")
|
||||
def _attached(self, sessionlib):
|
||||
return self.session_id is not None and \
|
||||
self.session_id in sessionlib._sessions
|
||||
|
||||
@property
|
||||
@util.dependencies("sqlalchemy.orm.session")
|
||||
def session(self, sessionlib):
|
||||
"""Return the owning :class:`.Session` for this instance,
|
||||
or ``None`` if none available."""
|
||||
return sessionlib._state_session(self)
|
||||
|
||||
@property
|
||||
def object(self):
|
||||
"""Return the mapped object represented by this
|
||||
:class:`.InstanceState`."""
|
||||
return self.obj()
|
||||
|
||||
@property
|
||||
def identity(self):
|
||||
"""Return the mapped identity of the mapped object.
|
||||
This is the primary key identity as persisted by the ORM
|
||||
which can always be passed directly to
|
||||
:meth:`.Query.get`.
|
||||
|
||||
Returns ``None`` if the object has no primary key identity.
|
||||
|
||||
.. note::
|
||||
An object which is transient or pending
|
||||
does **not** have a mapped identity until it is flushed,
|
||||
even if its attributes include primary key values.
|
||||
|
||||
"""
|
||||
if self.key is None:
|
||||
return None
|
||||
else:
|
||||
return self.key[1]
|
||||
|
||||
@property
|
||||
def identity_key(self):
|
||||
"""Return the identity key for the mapped object.
|
||||
|
||||
This is the key used to locate the object within
|
||||
the :attr:`.Session.identity_map` mapping. It contains
|
||||
the identity as returned by :attr:`.identity` within it.
|
||||
|
||||
|
||||
"""
|
||||
# TODO: just change .key to .identity_key across
|
||||
# the board ? probably
|
||||
return self.key
|
||||
|
||||
@util.memoized_property
|
||||
def parents(self):
|
||||
return {}
|
||||
|
||||
@util.memoized_property
|
||||
def pending(self):
|
||||
def _pending_mutations(self):
|
||||
return {}
|
||||
|
||||
@util.memoized_property
|
||||
def mapper(self):
|
||||
"""Return the :class:`.Mapper` used for this mapepd object."""
|
||||
return self.manager.mapper
|
||||
|
||||
@property
|
||||
def has_identity(self):
|
||||
"""Return ``True`` if this object has an identity key.
|
||||
|
||||
This should always have the same value as the
|
||||
expression ``state.persistent or state.detached``.
|
||||
|
||||
"""
|
||||
return bool(self.key)
|
||||
|
||||
def detach(self):
|
||||
self.session_id = None
|
||||
def _detach(self):
|
||||
self.session_id = self._strong_obj = None
|
||||
|
||||
def dispose(self):
|
||||
self.detach()
|
||||
def _dispose(self):
|
||||
self._detach()
|
||||
del self.obj
|
||||
|
||||
def _cleanup(self, ref):
|
||||
@@ -71,7 +172,7 @@ class InstanceState(object):
|
||||
instance_dict.discard(self)
|
||||
|
||||
self.callables = {}
|
||||
self.session_id = None
|
||||
self.session_id = self._strong_obj = None
|
||||
del self.obj
|
||||
|
||||
def obj(self):
|
||||
@@ -81,19 +182,16 @@ class InstanceState(object):
|
||||
def dict(self):
|
||||
o = self.obj()
|
||||
if o is not None:
|
||||
return attributes.instance_dict(o)
|
||||
return base.instance_dict(o)
|
||||
else:
|
||||
return {}
|
||||
|
||||
def initialize_instance(*mixed, **kwargs):
|
||||
def _initialize_instance(*mixed, **kwargs):
|
||||
self, instance, args = mixed[0], mixed[1], mixed[2:]
|
||||
manager = self.manager
|
||||
|
||||
manager.dispatch.init(self, args, kwargs)
|
||||
|
||||
#if manager.mutable_attributes:
|
||||
# assert self.__class__ is MutableAttrInstanceState
|
||||
|
||||
try:
|
||||
return manager.original_init(*mixed[1:], **kwargs)
|
||||
except:
|
||||
@@ -106,49 +204,29 @@ class InstanceState(object):
|
||||
def get_impl(self, 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, dict_, key, passive=PASSIVE_OFF):
|
||||
"""Return a list of tuples (state, obj) for the given
|
||||
key.
|
||||
|
||||
returns an empty list if the value is None/empty/PASSIVE_NO_RESULT
|
||||
"""
|
||||
|
||||
impl = self.manager[key].impl
|
||||
x = impl.get(self, dict_, passive=passive)
|
||||
if x is PASSIVE_NO_RESULT or x is None:
|
||||
return []
|
||||
elif hasattr(impl, 'get_collection'):
|
||||
return [
|
||||
(attributes.instance_state(o), o) for o in
|
||||
impl.get_collection(self, dict_, x, passive=passive)
|
||||
]
|
||||
else:
|
||||
return [(attributes.instance_state(x), x)]
|
||||
def _get_pending_mutation(self, key):
|
||||
if key not in self._pending_mutations:
|
||||
self._pending_mutations[key] = PendingCollection()
|
||||
return self._pending_mutations[key]
|
||||
|
||||
def __getstate__(self):
|
||||
d = {'instance':self.obj()}
|
||||
d.update(
|
||||
state_dict = {'instance': self.obj()}
|
||||
state_dict.update(
|
||||
(k, self.__dict__[k]) for k in (
|
||||
'committed_state', 'pending', 'modified', 'expired',
|
||||
'callables', 'key', 'parents', 'load_options', 'mutable_dict',
|
||||
'committed_state', '_pending_mutations', 'modified', 'expired',
|
||||
'callables', 'key', 'parents', 'load_options',
|
||||
'class_',
|
||||
) if k in self.__dict__
|
||||
)
|
||||
if self.load_path:
|
||||
d['load_path'] = interfaces.serialize_path(self.load_path)
|
||||
state_dict['load_path'] = self.load_path.serialize()
|
||||
|
||||
self.manager.dispatch.pickle(self, d)
|
||||
state_dict['manager'] = self.manager._serialize(self, state_dict)
|
||||
|
||||
return d
|
||||
return state_dict
|
||||
|
||||
def __setstate__(self, state):
|
||||
from sqlalchemy.orm import instrumentation
|
||||
inst = state['instance']
|
||||
def __setstate__(self, state_dict):
|
||||
inst = state_dict['instance']
|
||||
if inst is not None:
|
||||
self.obj = weakref.ref(inst, self._cleanup)
|
||||
self.class_ = inst.__class__
|
||||
@@ -157,60 +235,49 @@ class InstanceState(object):
|
||||
# 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(
|
||||
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.configured:
|
||||
mapperlib.configure_mappers()
|
||||
self.class_ = state_dict['class_']
|
||||
|
||||
self.committed_state = state.get('committed_state', {})
|
||||
self.pending = state.get('pending', {})
|
||||
self.parents = state.get('parents', {})
|
||||
self.modified = state.get('modified', False)
|
||||
self.expired = state.get('expired', False)
|
||||
self.callables = state.get('callables', {})
|
||||
|
||||
if self.modified:
|
||||
self._strong_obj = inst
|
||||
self.committed_state = state_dict.get('committed_state', {})
|
||||
self._pending_mutations = state_dict.get('_pending_mutations', {})
|
||||
self.parents = state_dict.get('parents', {})
|
||||
self.modified = state_dict.get('modified', False)
|
||||
self.expired = state_dict.get('expired', False)
|
||||
self.callables = state_dict.get('callables', {})
|
||||
|
||||
self.__dict__.update([
|
||||
(k, state[k]) for k in (
|
||||
'key', 'load_options', 'mutable_dict'
|
||||
) if k in state
|
||||
(k, state_dict[k]) for k in (
|
||||
'key', 'load_options',
|
||||
) if k in state_dict
|
||||
])
|
||||
|
||||
if 'load_path' in state:
|
||||
self.load_path = interfaces.deserialize_path(state['load_path'])
|
||||
if 'load_path' in state_dict:
|
||||
self.load_path = PathRegistry.\
|
||||
deserialize(state_dict['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)
|
||||
state_dict['manager'](self, inst, state_dict)
|
||||
|
||||
def initialize(self, key):
|
||||
def _initialize(self, key):
|
||||
"""Set this attribute to an empty value or collection,
|
||||
based on the AttributeImpl in use."""
|
||||
|
||||
self.manager.get_impl(key).initialize(self, self.dict)
|
||||
|
||||
def reset(self, dict_, key):
|
||||
def _reset(self, dict_, key):
|
||||
"""Remove the given attribute and any
|
||||
callables associated with it."""
|
||||
|
||||
dict_.pop(key, None)
|
||||
old = dict_.pop(key, None)
|
||||
if old is not None and self.manager[key].impl.collection:
|
||||
self.manager[key].impl._invalidate_collection(old)
|
||||
self.callables.pop(key, None)
|
||||
|
||||
def expire_attribute_pre_commit(self, dict_, key):
|
||||
def _expire_attribute_pre_commit(self, dict_, key):
|
||||
"""a fast expire that can be called by column loaders during a load.
|
||||
|
||||
The additional bookkeeping is finished up in commit_all().
|
||||
|
||||
Should only be called for scalar attributes.
|
||||
|
||||
This method is actually called a lot with joined-table
|
||||
loading, when the second table isn't present in the result.
|
||||
|
||||
@@ -218,65 +285,73 @@ class InstanceState(object):
|
||||
dict_.pop(key, None)
|
||||
self.callables[key] = self
|
||||
|
||||
def set_callable(self, dict_, key, callable_):
|
||||
"""Remove the given attribute and set the given callable
|
||||
as a loader."""
|
||||
@classmethod
|
||||
def _row_processor(cls, manager, fn, key):
|
||||
impl = manager[key].impl
|
||||
if impl.collection:
|
||||
def _set_callable(state, dict_, row):
|
||||
old = dict_.pop(key, None)
|
||||
if old is not None:
|
||||
impl._invalidate_collection(old)
|
||||
state.callables[key] = fn
|
||||
else:
|
||||
def _set_callable(state, dict_, row):
|
||||
state.callables[key] = fn
|
||||
return _set_callable
|
||||
|
||||
dict_.pop(key, None)
|
||||
self.callables[key] = callable_
|
||||
|
||||
def expire(self, dict_, modified_set):
|
||||
def _expire(self, dict_, modified_set):
|
||||
self.expired = True
|
||||
if self.modified:
|
||||
modified_set.discard(self)
|
||||
|
||||
self.modified = False
|
||||
self._strong_obj = None
|
||||
|
||||
self.committed_state.clear()
|
||||
|
||||
self.__dict__.pop('pending', None)
|
||||
self.__dict__.pop('mutable_dict', None)
|
||||
InstanceState._pending_mutations._reset(self)
|
||||
|
||||
# clear out 'parents' collection. not
|
||||
# entirely clear how we can best determine
|
||||
# which to remove, or not.
|
||||
self.__dict__.pop('parents', None)
|
||||
InstanceState.parents._reset(self)
|
||||
|
||||
for key in self.manager:
|
||||
impl = self.manager[key].impl
|
||||
if impl.accepts_scalar_loader and \
|
||||
(impl.expire_missing or key in dict_):
|
||||
(impl.expire_missing or key in dict_):
|
||||
self.callables[key] = self
|
||||
dict_.pop(key, None)
|
||||
old = dict_.pop(key, None)
|
||||
if impl.collection and old is not None:
|
||||
impl._invalidate_collection(old)
|
||||
|
||||
self.manager.dispatch.expire(self, None)
|
||||
|
||||
def expire_attributes(self, dict_, attribute_names):
|
||||
pending = self.__dict__.get('pending', None)
|
||||
mutable_dict = self.mutable_dict
|
||||
def _expire_attributes(self, dict_, attribute_names):
|
||||
pending = self.__dict__.get('_pending_mutations', None)
|
||||
|
||||
for key in attribute_names:
|
||||
impl = self.manager[key].impl
|
||||
if impl.accepts_scalar_loader:
|
||||
self.callables[key] = self
|
||||
dict_.pop(key, None)
|
||||
old = dict_.pop(key, None)
|
||||
if impl.collection and old is not None:
|
||||
impl._invalidate_collection(old)
|
||||
|
||||
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):
|
||||
def __call__(self, state, passive):
|
||||
"""__call__ allows the InstanceState to act as a deferred
|
||||
callable for loading expired attributes, which is also
|
||||
serializable (picklable).
|
||||
|
||||
"""
|
||||
|
||||
if passive is PASSIVE_NO_FETCH:
|
||||
if not passive & SQL_OK:
|
||||
return PASSIVE_NO_RESULT
|
||||
|
||||
toload = self.expired_attributes.\
|
||||
@@ -305,7 +380,6 @@ class InstanceState(object):
|
||||
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.
|
||||
@@ -318,6 +392,13 @@ class InstanceState(object):
|
||||
difference(self.committed_state).\
|
||||
difference(self.dict)
|
||||
|
||||
@property
|
||||
def _unloaded_non_object(self):
|
||||
return self.unloaded.intersection(
|
||||
attr for attr in self.manager
|
||||
if self.manager[attr].impl.accepts_scalar_loader
|
||||
)
|
||||
|
||||
@property
|
||||
def expired_attributes(self):
|
||||
"""Return the set of keys which are 'expired' to be loaded by
|
||||
@@ -333,10 +414,9 @@ class InstanceState(object):
|
||||
def _instance_dict(self):
|
||||
return None
|
||||
|
||||
def _is_really_none(self):
|
||||
return self.obj()
|
||||
|
||||
def modified_event(self, dict_, attr, previous, collection=False):
|
||||
def _modified_event(self, dict_, attr, previous, collection=False):
|
||||
if not attr.send_modified_events:
|
||||
return
|
||||
if attr.key not in self.committed_state:
|
||||
if collection:
|
||||
if previous is NEVER_SET:
|
||||
@@ -348,29 +428,33 @@ class InstanceState(object):
|
||||
|
||||
self.committed_state[attr.key] = previous
|
||||
|
||||
# the "or not self.modified" is defensive at
|
||||
# this point. The assertion below is expected
|
||||
# to be True:
|
||||
# assert self._strong_obj is None or self.modified
|
||||
|
||||
if self._strong_obj is None or not self.modified:
|
||||
if (self.session_id and self._strong_obj is None) \
|
||||
or not self.modified:
|
||||
instance_dict = self._instance_dict()
|
||||
if instance_dict:
|
||||
instance_dict._modified.add(self)
|
||||
|
||||
self._strong_obj = self.obj()
|
||||
if self._strong_obj is None:
|
||||
# only create _strong_obj link if attached
|
||||
# to a session
|
||||
|
||||
inst = self.obj()
|
||||
if self.session_id:
|
||||
self._strong_obj = inst
|
||||
|
||||
if inst 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)
|
||||
base.state_class_str(self)
|
||||
))
|
||||
self.modified = True
|
||||
|
||||
def commit(self, dict_, keys):
|
||||
def _commit(self, dict_, keys):
|
||||
"""Commit attributes.
|
||||
|
||||
This is used by a partial-attribute load operation to mark committed
|
||||
@@ -380,16 +464,8 @@ class InstanceState(object):
|
||||
this step if a value was not populated in state.dict.
|
||||
|
||||
"""
|
||||
class_manager = self.manager
|
||||
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)
|
||||
for key in keys:
|
||||
self.committed_state.pop(key, None)
|
||||
|
||||
self.expired = False
|
||||
|
||||
@@ -398,7 +474,7 @@ class InstanceState(object):
|
||||
intersection(dict_):
|
||||
del self.callables[key]
|
||||
|
||||
def commit_all(self, dict_, instance_dict=None):
|
||||
def _commit_all(self, dict_, instance_dict=None):
|
||||
"""commit all attributes unconditionally.
|
||||
|
||||
This is used after a flush() or a full load/refresh
|
||||
@@ -409,137 +485,112 @@ class InstanceState(object):
|
||||
- the "modified" flag is set to False
|
||||
- any "expired" markers/callables for attributes loaded are removed.
|
||||
|
||||
Attributes marked as "expired" can potentially remain "expired" after this step
|
||||
if a value was not populated in state.dict.
|
||||
Attributes marked as "expired" can potentially remain
|
||||
"expired" after this step if a value was not populated in state.dict.
|
||||
|
||||
"""
|
||||
self._commit_all_states([(self, dict_)], instance_dict)
|
||||
|
||||
self.committed_state.clear()
|
||||
self.__dict__.pop('pending', None)
|
||||
@classmethod
|
||||
def _commit_all_states(self, iter, instance_dict=None):
|
||||
"""Mass version of commit_all()."""
|
||||
|
||||
callables = self.callables
|
||||
for key in list(callables):
|
||||
if key in dict_ and callables[key] is self:
|
||||
del callables[key]
|
||||
for state, dict_ in iter:
|
||||
state.committed_state.clear()
|
||||
InstanceState._pending_mutations._reset(state)
|
||||
|
||||
for key in self.manager.mutable_attributes:
|
||||
if key in dict_:
|
||||
self.committed_state[key] = self.manager[key].impl.copy(dict_[key])
|
||||
callables = state.callables
|
||||
for key in list(callables):
|
||||
if key in dict_ and callables[key] is state:
|
||||
del callables[key]
|
||||
|
||||
if instance_dict and self.modified:
|
||||
instance_dict._modified.discard(self)
|
||||
if instance_dict and state.modified:
|
||||
instance_dict._modified.discard(state)
|
||||
|
||||
self.modified = self.expired = False
|
||||
self._strong_obj = None
|
||||
state.modified = state.expired = False
|
||||
state._strong_obj = None
|
||||
|
||||
class MutableAttrInstanceState(InstanceState):
|
||||
"""InstanceState implementation for objects that reference 'mutable'
|
||||
attributes.
|
||||
|
||||
Has a more involved "cleanup" handler that checks mutable attributes
|
||||
for changes upon dereference, resurrecting if needed.
|
||||
class AttributeState(object):
|
||||
"""Provide an inspection interface corresponding
|
||||
to a particular attribute on a particular mapped object.
|
||||
|
||||
The :class:`.AttributeState` object is accessed
|
||||
via the :attr:`.InstanceState.attrs` collection
|
||||
of a particular :class:`.InstanceState`::
|
||||
|
||||
from sqlalchemy import inspect
|
||||
|
||||
insp = inspect(some_mapped_object)
|
||||
attr_state = insp.attrs.some_attribute
|
||||
|
||||
"""
|
||||
|
||||
@util.memoized_property
|
||||
def mutable_dict(self):
|
||||
return {}
|
||||
|
||||
def _get_modified(self, dict_=None):
|
||||
if self.__dict__.get('modified', False):
|
||||
return True
|
||||
else:
|
||||
if dict_ is None:
|
||||
dict_ = self.dict
|
||||
for key in self.manager.mutable_attributes:
|
||||
if self.manager[key].impl.check_mutable_modified(self, dict_):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _set_modified(self, value):
|
||||
self.__dict__['modified'] = value
|
||||
|
||||
modified = property(_get_modified, _set_modified)
|
||||
def __init__(self, state, key):
|
||||
self.state = state
|
||||
self.key = key
|
||||
|
||||
@property
|
||||
def unmodified(self):
|
||||
"""a set of keys which have no uncommitted changes"""
|
||||
def loaded_value(self):
|
||||
"""The current value of this attribute as loaded from the database.
|
||||
|
||||
dict_ = self.dict
|
||||
|
||||
return set([
|
||||
key for key in self.manager
|
||||
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 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.
|
||||
|
||||
This would be called in the extremely rare
|
||||
race condition that the weakref returned None but
|
||||
the cleanup handler had not yet established the
|
||||
__resurrect callable as its replacement.
|
||||
If the value has not been loaded, or is otherwise not present
|
||||
in the object's dictionary, returns NO_VALUE.
|
||||
|
||||
"""
|
||||
if self.modified:
|
||||
self.obj = self.__resurrect
|
||||
return self.obj()
|
||||
else:
|
||||
return None
|
||||
return self.state.dict.get(self.key, NO_VALUE)
|
||||
|
||||
def reset(self, dict_, key):
|
||||
self.mutable_dict.pop(key, None)
|
||||
InstanceState.reset(self, dict_, key)
|
||||
@property
|
||||
def value(self):
|
||||
"""Return the value of this attribute.
|
||||
|
||||
def _cleanup(self, ref):
|
||||
"""weakref callback.
|
||||
|
||||
This method may be called by an asynchronous
|
||||
gc.
|
||||
|
||||
If the state shows pending changes, the weakref
|
||||
is replaced by the __resurrect callable which will
|
||||
re-establish an object reference on next access,
|
||||
else removes this InstanceState from the owning
|
||||
identity map, if any.
|
||||
This operation is equivalent to accessing the object's
|
||||
attribute directly or via ``getattr()``, and will fire
|
||||
off any pending loader callables if needed.
|
||||
|
||||
"""
|
||||
if self._get_modified(self.mutable_dict):
|
||||
self.obj = self.__resurrect
|
||||
else:
|
||||
instance_dict = self._instance_dict()
|
||||
if instance_dict:
|
||||
instance_dict.discard(self)
|
||||
self.dispose()
|
||||
return self.state.manager[self.key].__get__(
|
||||
self.state.obj(), self.state.class_)
|
||||
|
||||
def __resurrect(self):
|
||||
"""A substitute for the obj() weakref function which resurrects."""
|
||||
@property
|
||||
def history(self):
|
||||
"""Return the current pre-flush change history for
|
||||
this attribute, via the :class:`.History` interface.
|
||||
|
||||
# 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)
|
||||
This method will **not** emit loader callables if the value of the
|
||||
attribute is unloaded.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.AttributeState.load_history` - retrieve history
|
||||
using loader callables if the value is not locally present.
|
||||
|
||||
:func:`.attributes.get_history` - underlying function
|
||||
|
||||
"""
|
||||
return self.state.get_history(self.key,
|
||||
PASSIVE_NO_INITIALIZE)
|
||||
|
||||
def load_history(self):
|
||||
"""Return the current pre-flush change history for
|
||||
this attribute, via the :class:`.History` interface.
|
||||
|
||||
This method **will** emit loader callables if the value of the
|
||||
attribute is unloaded.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:attr:`.AttributeState.history`
|
||||
|
||||
:func:`.attributes.get_history` - underlying function
|
||||
|
||||
.. versionadded:: 0.9.0
|
||||
|
||||
"""
|
||||
return self.state.get_history(self.key,
|
||||
PASSIVE_OFF ^ INIT_OK)
|
||||
|
||||
# re-establishes identity attributes from the key
|
||||
self.manager.dispatch.resurrect(self)
|
||||
|
||||
return obj
|
||||
|
||||
class PendingCollection(object):
|
||||
"""A writable placeholder for an unloaded collection.
|
||||
@@ -564,4 +615,3 @@ class PendingCollection(object):
|
||||
self.added_items.remove(value)
|
||||
else:
|
||||
self.deleted_items.add(value)
|
||||
|
||||
|
||||
+471
-464
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,924 @@
|
||||
# orm/strategy_options.py
|
||||
# Copyright (C) 2005-2014 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 .interfaces import MapperOption, PropComparator
|
||||
from .. import util
|
||||
from ..sql.base import _generative, Generative
|
||||
from .. import exc as sa_exc, inspect
|
||||
from .base import _is_aliased_class, _class_to_mapper
|
||||
from . import util as orm_util
|
||||
from .path_registry import PathRegistry, TokenRegistry, \
|
||||
_WILDCARD_TOKEN, _DEFAULT_TOKEN
|
||||
|
||||
class Load(Generative, MapperOption):
|
||||
"""Represents loader options which modify the state of a
|
||||
:class:`.Query` in order to affect how various mapped attributes are loaded.
|
||||
|
||||
.. versionadded:: 0.9.0 The :meth:`.Load` system is a new foundation for
|
||||
the existing system of loader options, including options such as
|
||||
:func:`.orm.joinedload`, :func:`.orm.defer`, and others. In particular,
|
||||
it introduces a new method-chained system that replaces the need for
|
||||
dot-separated paths as well as "_all()" options such as :func:`.orm.joinedload_all`.
|
||||
|
||||
A :class:`.Load` object can be used directly or indirectly. To use one
|
||||
directly, instantiate given the parent class. This style of usage is
|
||||
useful when dealing with a :class:`.Query` that has multiple entities,
|
||||
or when producing a loader option that can be applied generically to
|
||||
any style of query::
|
||||
|
||||
myopt = Load(MyClass).joinedload("widgets")
|
||||
|
||||
The above ``myopt`` can now be used with :meth:`.Query.options`::
|
||||
|
||||
session.query(MyClass).options(myopt)
|
||||
|
||||
The :class:`.Load` construct is invoked indirectly whenever one makes use
|
||||
of the various loader options that are present in ``sqlalchemy.orm``, including
|
||||
options such as :func:`.orm.joinedload`, :func:`.orm.defer`, :func:`.orm.subqueryload`,
|
||||
and all the rest. These constructs produce an "anonymous" form of the
|
||||
:class:`.Load` object which tracks attributes and options, but is not linked
|
||||
to a parent class until it is associated with a parent :class:`.Query`::
|
||||
|
||||
# produce "unbound" Load object
|
||||
myopt = joinedload("widgets")
|
||||
|
||||
# when applied using options(), the option is "bound" to the
|
||||
# class observed in the given query, e.g. MyClass
|
||||
session.query(MyClass).options(myopt)
|
||||
|
||||
Whether the direct or indirect style is used, the :class:`.Load` object
|
||||
returned now represents a specific "path" along the entities of a :class:`.Query`.
|
||||
This path can be traversed using a standard method-chaining approach.
|
||||
Supposing a class hierarchy such as ``User``, ``User.addresses -> Address``,
|
||||
``User.orders -> Order`` and ``Order.items -> Item``, we can specify a variety
|
||||
of loader options along each element in the "path"::
|
||||
|
||||
session.query(User).options(
|
||||
joinedload("addresses"),
|
||||
subqueryload("orders").joinedload("items")
|
||||
)
|
||||
|
||||
Where above, the ``addresses`` collection will be joined-loaded, the
|
||||
``orders`` collection will be subquery-loaded, and within that subquery load
|
||||
the ``items`` collection will be joined-loaded.
|
||||
|
||||
|
||||
"""
|
||||
def __init__(self, entity):
|
||||
insp = inspect(entity)
|
||||
self.path = insp._path_registry
|
||||
self.context = {}
|
||||
self.local_opts = {}
|
||||
|
||||
def _generate(self):
|
||||
cloned = super(Load, self)._generate()
|
||||
cloned.local_opts = {}
|
||||
return cloned
|
||||
|
||||
strategy = None
|
||||
propagate_to_loaders = False
|
||||
|
||||
def process_query(self, query):
|
||||
self._process(query, True)
|
||||
|
||||
def process_query_conditionally(self, query):
|
||||
self._process(query, False)
|
||||
|
||||
def _process(self, query, raiseerr):
|
||||
current_path = query._current_path
|
||||
if current_path:
|
||||
for (token, start_path), loader in self.context.items():
|
||||
chopped_start_path = self._chop_path(start_path, current_path)
|
||||
if chopped_start_path is not None:
|
||||
query._attributes[(token, chopped_start_path)] = loader
|
||||
else:
|
||||
query._attributes.update(self.context)
|
||||
|
||||
def _generate_path(self, path, attr, wildcard_key, raiseerr=True):
|
||||
if raiseerr and not path.has_entity:
|
||||
if isinstance(path, TokenRegistry):
|
||||
raise sa_exc.ArgumentError(
|
||||
"Wildcard token cannot be followed by another entity")
|
||||
else:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Attribute '%s' of entity '%s' does not "
|
||||
"refer to a mapped entity" %
|
||||
(path.prop.key, path.parent.entity)
|
||||
)
|
||||
|
||||
if isinstance(attr, util.string_types):
|
||||
default_token = attr.endswith(_DEFAULT_TOKEN)
|
||||
if attr.endswith(_WILDCARD_TOKEN) or default_token:
|
||||
if default_token:
|
||||
self.propagate_to_loaders = False
|
||||
if wildcard_key:
|
||||
attr = "%s:%s" % (wildcard_key, attr)
|
||||
return path.token(attr)
|
||||
|
||||
try:
|
||||
# use getattr on the class to work around
|
||||
# synonyms, hybrids, etc.
|
||||
attr = getattr(path.entity.class_, attr)
|
||||
except AttributeError:
|
||||
if raiseerr:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Can't find property named '%s' on the "
|
||||
"mapped entity %s in this Query. " % (
|
||||
attr, path.entity)
|
||||
)
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
attr = attr.property
|
||||
|
||||
path = path[attr]
|
||||
else:
|
||||
prop = attr.property
|
||||
|
||||
if not prop.parent.common_parent(path.mapper):
|
||||
if raiseerr:
|
||||
raise sa_exc.ArgumentError("Attribute '%s' does not "
|
||||
"link from element '%s'" % (attr, path.entity))
|
||||
else:
|
||||
return None
|
||||
|
||||
if getattr(attr, '_of_type', None):
|
||||
ac = attr._of_type
|
||||
ext_info = inspect(ac)
|
||||
|
||||
path_element = ext_info.mapper
|
||||
if not ext_info.is_aliased_class:
|
||||
ac = orm_util.with_polymorphic(
|
||||
ext_info.mapper.base_mapper,
|
||||
ext_info.mapper, aliased=True,
|
||||
_use_mapper_path=True)
|
||||
path.entity_path[prop].set(self.context,
|
||||
"path_with_polymorphic", inspect(ac))
|
||||
path = path[prop][path_element]
|
||||
else:
|
||||
path = path[prop]
|
||||
|
||||
if path.has_entity:
|
||||
path = path.entity_path
|
||||
return path
|
||||
|
||||
def _coerce_strat(self, strategy):
|
||||
if strategy is not None:
|
||||
strategy = tuple(sorted(strategy.items()))
|
||||
return strategy
|
||||
|
||||
@_generative
|
||||
def set_relationship_strategy(self, attr, strategy, propagate_to_loaders=True):
|
||||
strategy = self._coerce_strat(strategy)
|
||||
|
||||
self.propagate_to_loaders = propagate_to_loaders
|
||||
# if the path is a wildcard, this will set propagate_to_loaders=False
|
||||
self.path = self._generate_path(self.path, attr, "relationship")
|
||||
self.strategy = strategy
|
||||
if strategy is not None:
|
||||
self._set_path_strategy()
|
||||
|
||||
@_generative
|
||||
def set_column_strategy(self, attrs, strategy, opts=None):
|
||||
strategy = self._coerce_strat(strategy)
|
||||
|
||||
for attr in attrs:
|
||||
path = self._generate_path(self.path, attr, "column")
|
||||
cloned = self._generate()
|
||||
cloned.strategy = strategy
|
||||
cloned.path = path
|
||||
cloned.propagate_to_loaders = True
|
||||
if opts:
|
||||
cloned.local_opts.update(opts)
|
||||
cloned._set_path_strategy()
|
||||
|
||||
def _set_path_strategy(self):
|
||||
if self.path.has_entity:
|
||||
self.path.parent.set(self.context, "loader", self)
|
||||
else:
|
||||
self.path.set(self.context, "loader", self)
|
||||
|
||||
def __getstate__(self):
|
||||
d = self.__dict__.copy()
|
||||
d["path"] = self.path.serialize()
|
||||
return d
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__dict__.update(state)
|
||||
self.path = PathRegistry.deserialize(self.path)
|
||||
|
||||
def _chop_path(self, to_chop, path):
|
||||
i = -1
|
||||
|
||||
for i, (c_token, p_token) in enumerate(zip(to_chop, path.path)):
|
||||
if isinstance(c_token, util.string_types):
|
||||
# TODO: this is approximated from the _UnboundLoad
|
||||
# version and probably has issues, not fully covered.
|
||||
|
||||
if i == 0 and c_token.endswith(':' + _DEFAULT_TOKEN):
|
||||
return to_chop
|
||||
elif c_token != 'relationship:%s' % (_WILDCARD_TOKEN,) and c_token != p_token.key:
|
||||
return None
|
||||
|
||||
if c_token is p_token:
|
||||
continue
|
||||
else:
|
||||
return None
|
||||
return to_chop[i+1:]
|
||||
|
||||
|
||||
class _UnboundLoad(Load):
|
||||
"""Represent a loader option that isn't tied to a root entity.
|
||||
|
||||
The loader option will produce an entity-linked :class:`.Load`
|
||||
object when it is passed :meth:`.Query.options`.
|
||||
|
||||
This provides compatibility with the traditional system
|
||||
of freestanding options, e.g. ``joinedload('x.y.z')``.
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
self.path = ()
|
||||
self._to_bind = set()
|
||||
self.local_opts = {}
|
||||
|
||||
_is_chain_link = False
|
||||
|
||||
def _set_path_strategy(self):
|
||||
self._to_bind.add(self)
|
||||
|
||||
def _generate_path(self, path, attr, wildcard_key):
|
||||
if wildcard_key and isinstance(attr, util.string_types) and \
|
||||
attr in (_WILDCARD_TOKEN, _DEFAULT_TOKEN):
|
||||
if attr == _DEFAULT_TOKEN:
|
||||
self.propagate_to_loaders = False
|
||||
attr = "%s:%s" % (wildcard_key, attr)
|
||||
|
||||
return path + (attr, )
|
||||
|
||||
def __getstate__(self):
|
||||
d = self.__dict__.copy()
|
||||
d['path'] = ret = []
|
||||
for token in util.to_list(self.path):
|
||||
if isinstance(token, PropComparator):
|
||||
ret.append((token._parentmapper.class_, token.key))
|
||||
else:
|
||||
ret.append(token)
|
||||
return d
|
||||
|
||||
def __setstate__(self, state):
|
||||
ret = []
|
||||
for key in state['path']:
|
||||
if isinstance(key, tuple):
|
||||
cls, propkey = key
|
||||
ret.append(getattr(cls, propkey))
|
||||
else:
|
||||
ret.append(key)
|
||||
state['path'] = tuple(ret)
|
||||
self.__dict__ = state
|
||||
|
||||
def _process(self, query, raiseerr):
|
||||
for val in self._to_bind:
|
||||
val._bind_loader(query, query._attributes, raiseerr)
|
||||
|
||||
@classmethod
|
||||
def _from_keys(self, meth, keys, chained, kw):
|
||||
opt = _UnboundLoad()
|
||||
|
||||
def _split_key(key):
|
||||
if isinstance(key, util.string_types):
|
||||
# coerce fooload('*') into "default loader strategy"
|
||||
if key == _WILDCARD_TOKEN:
|
||||
return (_DEFAULT_TOKEN, )
|
||||
# coerce fooload(".*") into "wildcard on default entity"
|
||||
elif key.startswith("." + _WILDCARD_TOKEN):
|
||||
key = key[1:]
|
||||
return key.split(".")
|
||||
else:
|
||||
return (key,)
|
||||
all_tokens = [token for key in keys for token in _split_key(key)]
|
||||
|
||||
for token in all_tokens[0:-1]:
|
||||
if chained:
|
||||
opt = meth(opt, token, **kw)
|
||||
else:
|
||||
opt = opt.defaultload(token)
|
||||
opt._is_chain_link = True
|
||||
|
||||
opt = meth(opt, all_tokens[-1], **kw)
|
||||
opt._is_chain_link = False
|
||||
|
||||
return opt
|
||||
|
||||
|
||||
def _chop_path(self, to_chop, path):
|
||||
i = -1
|
||||
for i, (c_token, (p_mapper, p_prop)) in enumerate(zip(to_chop, path.pairs())):
|
||||
if isinstance(c_token, util.string_types):
|
||||
if i == 0 and c_token.endswith(':' + _DEFAULT_TOKEN):
|
||||
return to_chop
|
||||
elif c_token != 'relationship:%s' % (_WILDCARD_TOKEN,) and c_token != p_prop.key:
|
||||
return None
|
||||
elif isinstance(c_token, PropComparator):
|
||||
if c_token.property is not p_prop:
|
||||
return None
|
||||
else:
|
||||
i += 1
|
||||
|
||||
return to_chop[i:]
|
||||
|
||||
|
||||
def _bind_loader(self, query, context, raiseerr):
|
||||
start_path = self.path
|
||||
# _current_path implies we're in a
|
||||
# secondary load with an existing path
|
||||
|
||||
current_path = query._current_path
|
||||
if current_path:
|
||||
start_path = self._chop_path(start_path, current_path)
|
||||
|
||||
if not start_path:
|
||||
return None
|
||||
|
||||
token = start_path[0]
|
||||
if isinstance(token, util.string_types):
|
||||
entity = self._find_entity_basestring(query, token, raiseerr)
|
||||
elif isinstance(token, PropComparator):
|
||||
prop = token.property
|
||||
entity = self._find_entity_prop_comparator(
|
||||
query,
|
||||
prop.key,
|
||||
token._parententity,
|
||||
raiseerr)
|
||||
|
||||
else:
|
||||
raise sa_exc.ArgumentError(
|
||||
"mapper option expects "
|
||||
"string key or list of attributes")
|
||||
|
||||
if not entity:
|
||||
return
|
||||
|
||||
path_element = entity.entity_zero
|
||||
|
||||
# transfer our entity-less state into a Load() object
|
||||
# with a real entity path.
|
||||
loader = Load(path_element)
|
||||
loader.context = context
|
||||
loader.strategy = self.strategy
|
||||
|
||||
path = loader.path
|
||||
for token in start_path:
|
||||
loader.path = path = loader._generate_path(
|
||||
loader.path, token, None, raiseerr)
|
||||
if path is None:
|
||||
return
|
||||
|
||||
loader.local_opts.update(self.local_opts)
|
||||
|
||||
if loader.path.has_entity:
|
||||
effective_path = loader.path.parent
|
||||
else:
|
||||
effective_path = loader.path
|
||||
|
||||
# prioritize "first class" options over those
|
||||
# that were "links in the chain", e.g. "x" and "y" in someload("x.y.z")
|
||||
# versus someload("x") / someload("x.y")
|
||||
if self._is_chain_link:
|
||||
effective_path.setdefault(context, "loader", loader)
|
||||
else:
|
||||
effective_path.set(context, "loader", loader)
|
||||
|
||||
def _find_entity_prop_comparator(self, query, token, mapper, raiseerr):
|
||||
if _is_aliased_class(mapper):
|
||||
searchfor = mapper
|
||||
else:
|
||||
searchfor = _class_to_mapper(mapper)
|
||||
for ent in query._mapper_entities:
|
||||
if ent.corresponds_to(searchfor):
|
||||
return ent
|
||||
else:
|
||||
if raiseerr:
|
||||
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):
|
||||
if token.endswith(':' + _WILDCARD_TOKEN):
|
||||
if len(list(query._mapper_entities)) != 1:
|
||||
if raiseerr:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Wildcard loader can only be used with exactly "
|
||||
"one entity. Use Load(ent) to specify "
|
||||
"specific entities.")
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
class loader_option(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __call__(self, fn):
|
||||
self.name = name = fn.__name__
|
||||
self.fn = fn
|
||||
if hasattr(Load, name):
|
||||
raise TypeError("Load class already has a %s method." % (name))
|
||||
setattr(Load, name, fn)
|
||||
|
||||
return self
|
||||
|
||||
def _add_unbound_fn(self, fn):
|
||||
self._unbound_fn = fn
|
||||
fn_doc = self.fn.__doc__
|
||||
self.fn.__doc__ = """Produce a new :class:`.Load` object with the
|
||||
:func:`.orm.%(name)s` option applied.
|
||||
|
||||
See :func:`.orm.%(name)s` for usage examples.
|
||||
|
||||
""" % {"name": self.name}
|
||||
|
||||
fn.__doc__ = fn_doc
|
||||
return self
|
||||
|
||||
def _add_unbound_all_fn(self, fn):
|
||||
self._unbound_all_fn = fn
|
||||
fn.__doc__ = """Produce a standalone "all" option for :func:`.orm.%(name)s`.
|
||||
|
||||
.. deprecated:: 0.9.0
|
||||
|
||||
The "_all()" style is replaced by method chaining, e.g.::
|
||||
|
||||
session.query(MyClass).options(
|
||||
%(name)s("someattribute").%(name)s("anotherattribute")
|
||||
)
|
||||
|
||||
""" % {"name": self.name}
|
||||
return self
|
||||
|
||||
@loader_option()
|
||||
def contains_eager(loadopt, attr, alias=None):
|
||||
"""Indicate that the given attribute should be eagerly loaded from
|
||||
columns stated manually in the query.
|
||||
|
||||
This function is part of the :class:`.Load` interface and supports
|
||||
both method-chained and standalone operation.
|
||||
|
||||
The option is used in conjunction with an explicit join that loads
|
||||
the desired rows, i.e.::
|
||||
|
||||
sess.query(Order).\\
|
||||
join(Order.user).\\
|
||||
options(contains_eager(Order.user))
|
||||
|
||||
The above query would join from the ``Order`` entity to its related
|
||||
``User`` entity, and the returned ``Order`` objects would have the
|
||||
``Order.user`` attribute pre-populated.
|
||||
|
||||
:func:`contains_eager` also accepts an `alias` argument, which is the
|
||||
string name of an alias, an :func:`~sqlalchemy.sql.expression.alias`
|
||||
construct, or an :func:`~sqlalchemy.orm.aliased` construct. Use this when
|
||||
the eagerly-loaded rows are to come from an aliased table::
|
||||
|
||||
user_alias = aliased(User)
|
||||
sess.query(Order).\\
|
||||
join((user_alias, Order.user)).\\
|
||||
options(contains_eager(Order.user, alias=user_alias))
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`contains_eager`
|
||||
|
||||
"""
|
||||
if alias is not None:
|
||||
if not isinstance(alias, str):
|
||||
info = inspect(alias)
|
||||
alias = info.selectable
|
||||
|
||||
cloned = loadopt.set_relationship_strategy(
|
||||
attr,
|
||||
{"lazy": "joined"},
|
||||
propagate_to_loaders=False
|
||||
)
|
||||
cloned.local_opts['eager_from_alias'] = alias
|
||||
return cloned
|
||||
|
||||
@contains_eager._add_unbound_fn
|
||||
def contains_eager(*keys, **kw):
|
||||
return _UnboundLoad()._from_keys(_UnboundLoad.contains_eager, keys, True, kw)
|
||||
|
||||
@loader_option()
|
||||
def load_only(loadopt, *attrs):
|
||||
"""Indicate that for a particular entity, only the given list
|
||||
of column-based attribute names should be loaded; all others will be
|
||||
deferred.
|
||||
|
||||
This function is part of the :class:`.Load` interface and supports
|
||||
both method-chained and standalone operation.
|
||||
|
||||
Example - given a class ``User``, load only the ``name`` and ``fullname``
|
||||
attributes::
|
||||
|
||||
session.query(User).options(load_only("name", "fullname"))
|
||||
|
||||
Example - given a relationship ``User.addresses -> Address``, specify
|
||||
subquery loading for the ``User.addresses`` collection, but on each ``Address``
|
||||
object load only the ``email_address`` attribute::
|
||||
|
||||
session.query(User).options(
|
||||
subqueryload("addreses").load_only("email_address")
|
||||
)
|
||||
|
||||
For a :class:`.Query` that has multiple entities, the lead entity can be
|
||||
specifically referred to using the :class:`.Load` constructor::
|
||||
|
||||
session.query(User, Address).join(User.addresses).options(
|
||||
Load(User).load_only("name", "fullname"),
|
||||
Load(Address).load_only("email_addres")
|
||||
)
|
||||
|
||||
|
||||
.. versionadded:: 0.9.0
|
||||
|
||||
"""
|
||||
cloned = loadopt.set_column_strategy(
|
||||
attrs,
|
||||
{"deferred": False, "instrument": True}
|
||||
)
|
||||
cloned.set_column_strategy("*",
|
||||
{"deferred": True, "instrument": True})
|
||||
return cloned
|
||||
|
||||
@load_only._add_unbound_fn
|
||||
def load_only(*attrs):
|
||||
return _UnboundLoad().load_only(*attrs)
|
||||
|
||||
@loader_option()
|
||||
def joinedload(loadopt, attr, innerjoin=None):
|
||||
"""Indicate that the given attribute should be loaded using joined
|
||||
eager loading.
|
||||
|
||||
This function is part of the :class:`.Load` interface and supports
|
||||
both method-chained and standalone operation.
|
||||
|
||||
examples::
|
||||
|
||||
# joined-load the "orders" collection on "User"
|
||||
query(User).options(joinedload(User.orders))
|
||||
|
||||
# joined-load Order.items and then Item.keywords
|
||||
query(Order).options(joinedload(Order.items).joinedload(Item.keywords))
|
||||
|
||||
# lazily load Order.items, but when Items are loaded,
|
||||
# joined-load the keywords collection
|
||||
query(Order).options(lazyload(Order.items).joinedload(Item.keywords))
|
||||
|
||||
:func:`.orm.joinedload` also accepts a keyword argument `innerjoin=True` which
|
||||
indicates using an inner join instead of an outer::
|
||||
|
||||
query(Order).options(joinedload(Order.user, innerjoin=True))
|
||||
|
||||
.. note::
|
||||
|
||||
The joins produced by :func:`.orm.joinedload` are **anonymously aliased**.
|
||||
The criteria by which the join proceeds cannot be modified, nor can the
|
||||
:class:`.Query` refer to these joins in any way, including ordering.
|
||||
|
||||
To produce a specific SQL JOIN which is explicitly available, use
|
||||
:meth:`.Query.join`. To combine explicit JOINs with eager loading
|
||||
of collections, use :func:`.orm.contains_eager`; see :ref:`contains_eager`.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`loading_toplevel`
|
||||
|
||||
:ref:`contains_eager`
|
||||
|
||||
:func:`.orm.subqueryload`
|
||||
|
||||
:func:`.orm.lazyload`
|
||||
|
||||
"""
|
||||
loader = loadopt.set_relationship_strategy(attr, {"lazy": "joined"})
|
||||
if innerjoin is not None:
|
||||
loader.local_opts['innerjoin'] = innerjoin
|
||||
return loader
|
||||
|
||||
@joinedload._add_unbound_fn
|
||||
def joinedload(*keys, **kw):
|
||||
return _UnboundLoad._from_keys(
|
||||
_UnboundLoad.joinedload, keys, False, kw)
|
||||
|
||||
@joinedload._add_unbound_all_fn
|
||||
def joinedload_all(*keys, **kw):
|
||||
return _UnboundLoad._from_keys(
|
||||
_UnboundLoad.joinedload, keys, True, kw)
|
||||
|
||||
|
||||
@loader_option()
|
||||
def subqueryload(loadopt, attr):
|
||||
"""Indicate that the given attribute should be loaded using
|
||||
subquery eager loading.
|
||||
|
||||
This function is part of the :class:`.Load` interface and supports
|
||||
both method-chained and standalone operation.
|
||||
|
||||
examples::
|
||||
|
||||
# subquery-load the "orders" collection on "User"
|
||||
query(User).options(subqueryload(User.orders))
|
||||
|
||||
# subquery-load Order.items and then Item.keywords
|
||||
query(Order).options(subqueryload(Order.items).subqueryload(Item.keywords))
|
||||
|
||||
# lazily load Order.items, but when Items are loaded,
|
||||
# subquery-load the keywords collection
|
||||
query(Order).options(lazyload(Order.items).subqueryload(Item.keywords))
|
||||
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`loading_toplevel`
|
||||
|
||||
:func:`.orm.joinedload`
|
||||
|
||||
:func:`.orm.lazyload`
|
||||
|
||||
"""
|
||||
return loadopt.set_relationship_strategy(attr, {"lazy": "subquery"})
|
||||
|
||||
@subqueryload._add_unbound_fn
|
||||
def subqueryload(*keys):
|
||||
return _UnboundLoad._from_keys(_UnboundLoad.subqueryload, keys, False, {})
|
||||
|
||||
@subqueryload._add_unbound_all_fn
|
||||
def subqueryload_all(*keys):
|
||||
return _UnboundLoad._from_keys(_UnboundLoad.subqueryload, keys, True, {})
|
||||
|
||||
@loader_option()
|
||||
def lazyload(loadopt, attr):
|
||||
"""Indicate that the given attribute should be loaded using "lazy"
|
||||
loading.
|
||||
|
||||
This function is part of the :class:`.Load` interface and supports
|
||||
both method-chained and standalone operation.
|
||||
|
||||
"""
|
||||
return loadopt.set_relationship_strategy(attr, {"lazy": "select"})
|
||||
|
||||
@lazyload._add_unbound_fn
|
||||
def lazyload(*keys):
|
||||
return _UnboundLoad._from_keys(_UnboundLoad.lazyload, keys, False, {})
|
||||
|
||||
@lazyload._add_unbound_all_fn
|
||||
def lazyload_all(*keys):
|
||||
return _UnboundLoad._from_keys(_UnboundLoad.lazyload, keys, True, {})
|
||||
|
||||
@loader_option()
|
||||
def immediateload(loadopt, attr):
|
||||
"""Indicate that the given attribute should be loaded using
|
||||
an immediate load with a per-attribute SELECT statement.
|
||||
|
||||
This function is part of the :class:`.Load` interface and supports
|
||||
both method-chained and standalone operation.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`loading_toplevel`
|
||||
|
||||
:func:`.orm.joinedload`
|
||||
|
||||
:func:`.orm.lazyload`
|
||||
|
||||
"""
|
||||
loader = loadopt.set_relationship_strategy(attr, {"lazy": "immediate"})
|
||||
return loader
|
||||
|
||||
@immediateload._add_unbound_fn
|
||||
def immediateload(*keys):
|
||||
return _UnboundLoad._from_keys(_UnboundLoad.immediateload, keys, False, {})
|
||||
|
||||
|
||||
@loader_option()
|
||||
def noload(loadopt, attr):
|
||||
"""Indicate that the given relationship attribute should remain unloaded.
|
||||
|
||||
This function is part of the :class:`.Load` interface and supports
|
||||
both method-chained and standalone operation.
|
||||
|
||||
:func:`.orm.noload` applies to :func:`.relationship` attributes; for
|
||||
column-based attributes, see :func:`.orm.defer`.
|
||||
|
||||
"""
|
||||
|
||||
return loadopt.set_relationship_strategy(attr, {"lazy": "noload"})
|
||||
|
||||
@noload._add_unbound_fn
|
||||
def noload(*keys):
|
||||
return _UnboundLoad._from_keys(_UnboundLoad.noload, keys, False, {})
|
||||
|
||||
@loader_option()
|
||||
def defaultload(loadopt, attr):
|
||||
"""Indicate an attribute should load using its default loader style.
|
||||
|
||||
This method is used to link to other loader options, such as
|
||||
to set the :func:`.orm.defer` option on a class that is linked to
|
||||
a relationship of the parent class being loaded, :func:`.orm.defaultload`
|
||||
can be used to navigate this path without changing the loading style
|
||||
of the relationship::
|
||||
|
||||
session.query(MyClass).options(defaultload("someattr").defer("some_column"))
|
||||
|
||||
.. seealso::
|
||||
|
||||
:func:`.orm.defer`
|
||||
|
||||
:func:`.orm.undefer`
|
||||
|
||||
"""
|
||||
return loadopt.set_relationship_strategy(
|
||||
attr,
|
||||
None
|
||||
)
|
||||
|
||||
@defaultload._add_unbound_fn
|
||||
def defaultload(*keys):
|
||||
return _UnboundLoad._from_keys(_UnboundLoad.defaultload, keys, False, {})
|
||||
|
||||
@loader_option()
|
||||
def defer(loadopt, key):
|
||||
"""Indicate that the given column-oriented attribute should be deferred, e.g.
|
||||
not loaded until accessed.
|
||||
|
||||
This function is part of the :class:`.Load` interface and supports
|
||||
both method-chained and standalone operation.
|
||||
|
||||
e.g.::
|
||||
|
||||
from sqlalchemy.orm import defer
|
||||
|
||||
session.query(MyClass).options(
|
||||
defer("attribute_one"),
|
||||
defer("attribute_two"))
|
||||
|
||||
session.query(MyClass).options(
|
||||
defer(MyClass.attribute_one),
|
||||
defer(MyClass.attribute_two))
|
||||
|
||||
To specify a deferred load of an attribute on a related class,
|
||||
the path can be specified one token at a time, specifying the loading
|
||||
style for each link along the chain. To leave the loading style
|
||||
for a link unchanged, use :func:`.orm.defaultload`::
|
||||
|
||||
session.query(MyClass).options(defaultload("someattr").defer("some_column"))
|
||||
|
||||
A :class:`.Load` object that is present on a certain path can have
|
||||
:meth:`.Load.defer` called multiple times, each will operate on the same
|
||||
parent entity::
|
||||
|
||||
|
||||
session.query(MyClass).options(
|
||||
defaultload("someattr").
|
||||
defer("some_column").
|
||||
defer("some_other_column").
|
||||
defer("another_column")
|
||||
)
|
||||
|
||||
:param key: Attribute to be deferred.
|
||||
|
||||
:param \*addl_attrs: Deprecated; this option supports the old 0.8 style
|
||||
of specifying a path as a series of attributes, which is now superseded
|
||||
by the method-chained style.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`deferred`
|
||||
|
||||
:func:`.orm.undefer`
|
||||
|
||||
"""
|
||||
return loadopt.set_column_strategy(
|
||||
(key, ),
|
||||
{"deferred": True, "instrument": True}
|
||||
)
|
||||
|
||||
|
||||
@defer._add_unbound_fn
|
||||
def defer(key, *addl_attrs):
|
||||
return _UnboundLoad._from_keys(_UnboundLoad.defer, (key, ) + addl_attrs, False, {})
|
||||
|
||||
@loader_option()
|
||||
def undefer(loadopt, key):
|
||||
"""Indicate that the given column-oriented attribute should be undeferred, e.g.
|
||||
specified within the SELECT statement of the entity as a whole.
|
||||
|
||||
The column being undeferred is typically set up on the mapping as a
|
||||
:func:`.deferred` attribute.
|
||||
|
||||
This function is part of the :class:`.Load` interface and supports
|
||||
both method-chained and standalone operation.
|
||||
|
||||
Examples::
|
||||
|
||||
# undefer two columns
|
||||
session.query(MyClass).options(undefer("col1"), undefer("col2"))
|
||||
|
||||
# undefer all columns specific to a single class using Load + *
|
||||
session.query(MyClass, MyOtherClass).options(Load(MyClass).undefer("*"))
|
||||
|
||||
:param key: Attribute to be undeferred.
|
||||
|
||||
:param \*addl_attrs: Deprecated; this option supports the old 0.8 style
|
||||
of specifying a path as a series of attributes, which is now superseded
|
||||
by the method-chained style.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`deferred`
|
||||
|
||||
:func:`.orm.defer`
|
||||
|
||||
:func:`.orm.undefer_group`
|
||||
|
||||
"""
|
||||
return loadopt.set_column_strategy(
|
||||
(key, ),
|
||||
{"deferred": False, "instrument": True}
|
||||
)
|
||||
|
||||
@undefer._add_unbound_fn
|
||||
def undefer(key, *addl_attrs):
|
||||
return _UnboundLoad._from_keys(_UnboundLoad.undefer, (key, ) + addl_attrs, False, {})
|
||||
|
||||
@loader_option()
|
||||
def undefer_group(loadopt, name):
|
||||
"""Indicate that columns within the given deferred group name should be undeferred.
|
||||
|
||||
The columns being undeferred are set up on the mapping as
|
||||
:func:`.deferred` attributes and include a "group" name.
|
||||
|
||||
E.g::
|
||||
|
||||
session.query(MyClass).options(undefer_group("large_attrs"))
|
||||
|
||||
To undefer a group of attributes on a related entity, the path can be
|
||||
spelled out using relationship loader options, such as :func:`.orm.defaultload`::
|
||||
|
||||
session.query(MyClass).options(defaultload("someattr").undefer_group("large_attrs"))
|
||||
|
||||
.. versionchanged:: 0.9.0 :func:`.orm.undefer_group` is now specific to a
|
||||
particiular entity load path.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`deferred`
|
||||
|
||||
:func:`.orm.defer`
|
||||
|
||||
:func:`.orm.undefer`
|
||||
|
||||
"""
|
||||
return loadopt.set_column_strategy(
|
||||
"*",
|
||||
None,
|
||||
{"undefer_group": name}
|
||||
)
|
||||
|
||||
@undefer_group._add_unbound_fn
|
||||
def undefer_group(name):
|
||||
return _UnboundLoad().undefer_group(name)
|
||||
|
||||
+34
-24
@@ -1,5 +1,5 @@
|
||||
# orm/sync.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 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
|
||||
@@ -9,7 +9,8 @@ between instances based on join conditions.
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm import exc, util as mapperutil, attributes
|
||||
from . import exc, util as orm_util, attributes
|
||||
|
||||
|
||||
def populate(source, source_mapper, dest, dest_mapper,
|
||||
synchronize_pairs, uowcommit, flag_cascaded_pks):
|
||||
@@ -42,38 +43,45 @@ def populate(source, source_mapper, dest, dest_mapper,
|
||||
r.references(l):
|
||||
uowcommit.attributes[("pk_cascaded", dest, r)] = True
|
||||
|
||||
|
||||
def clear(dest, dest_mapper, synchronize_pairs):
|
||||
for l, r in synchronize_pairs:
|
||||
if r.primary_key:
|
||||
raise AssertionError(
|
||||
"Dependency rule tried to blank-out primary key "
|
||||
"column '%s' on instance '%s'" %
|
||||
(r, mapperutil.state_str(dest))
|
||||
)
|
||||
"Dependency rule tried to blank-out primary key "
|
||||
"column '%s' on instance '%s'" %
|
||||
(r, orm_util.state_str(dest))
|
||||
)
|
||||
try:
|
||||
dest_mapper._set_state_attr_by_column(dest, dest.dict, r, None)
|
||||
except exc.UnmappedColumnError:
|
||||
_raise_col_to_prop(True, None, l, dest_mapper, r)
|
||||
|
||||
|
||||
def update(source, source_mapper, dest, old_prefix, synchronize_pairs):
|
||||
for l, r in synchronize_pairs:
|
||||
try:
|
||||
oldvalue = source_mapper._get_committed_attr_by_column(source.obj(), l)
|
||||
value = source_mapper._get_state_attr_by_column(source, source.dict, l)
|
||||
oldvalue = source_mapper._get_committed_attr_by_column(
|
||||
source.obj(), l)
|
||||
value = source_mapper._get_state_attr_by_column(
|
||||
source, source.dict, l)
|
||||
except exc.UnmappedColumnError:
|
||||
_raise_col_to_prop(False, source_mapper, l, None, r)
|
||||
dest[r.key] = value
|
||||
dest[old_prefix + r.key] = oldvalue
|
||||
|
||||
|
||||
def populate_dict(source, source_mapper, dict_, synchronize_pairs):
|
||||
for l, r in synchronize_pairs:
|
||||
try:
|
||||
value = source_mapper._get_state_attr_by_column(source, source.dict, l)
|
||||
value = source_mapper._get_state_attr_by_column(
|
||||
source, source.dict, l)
|
||||
except exc.UnmappedColumnError:
|
||||
_raise_col_to_prop(False, source_mapper, l, None, r)
|
||||
|
||||
dict_[r.key] = value
|
||||
|
||||
|
||||
def source_modified(uowcommit, source, source_mapper, synchronize_pairs):
|
||||
"""return true if the source object has changes from an old to a
|
||||
new value on the given synchronize pairs
|
||||
@@ -86,23 +94,25 @@ def source_modified(uowcommit, source, source_mapper, synchronize_pairs):
|
||||
_raise_col_to_prop(False, source_mapper, l, None, r)
|
||||
history = uowcommit.get_attribute_history(source, prop.key,
|
||||
attributes.PASSIVE_NO_INITIALIZE)
|
||||
return bool(history.deleted)
|
||||
if bool(history.deleted):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def _raise_col_to_prop(isdest, source_mapper, source_column, dest_mapper, dest_column):
|
||||
|
||||
def _raise_col_to_prop(isdest, source_mapper, source_column,
|
||||
dest_mapper, dest_column):
|
||||
if isdest:
|
||||
raise exc.UnmappedColumnError(
|
||||
"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, dest_mapper)
|
||||
)
|
||||
raise exc.UnmappedColumnError("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,
|
||||
dest_mapper))
|
||||
else:
|
||||
raise exc.UnmappedColumnError(
|
||||
"Can't execute sync rule for source column '%s'; mapper '%s' "
|
||||
"does not map this column. Try using an explicit `foreign_keys`"
|
||||
" collection which does not include destination column '%s' (or "
|
||||
"use a viewonly=True relation)." %
|
||||
(source_column, source_mapper, dest_column)
|
||||
)
|
||||
raise exc.UnmappedColumnError("Can't execute sync rule for "
|
||||
"source column '%s'; mapper '%s' does not map this "
|
||||
"column. Try using an explicit `foreign_keys` "
|
||||
"collection which does not include destination column "
|
||||
"'%s' (or use a viewonly=True relation)."
|
||||
% (source_column, source_mapper, dest_column))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# orm/unitofwork.py
|
||||
# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||
# Copyright (C) 2005-2014 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,11 +12,10 @@ organizes them in order of dependency, and executes.
|
||||
|
||||
"""
|
||||
|
||||
from sqlalchemy import util, event
|
||||
from sqlalchemy.util import topological
|
||||
from sqlalchemy.orm import attributes, interfaces, persistence
|
||||
from sqlalchemy.orm import util as mapperutil
|
||||
session = util.importlater("sqlalchemy.orm", "session")
|
||||
from .. import util, event
|
||||
from ..util import topological
|
||||
from . import attributes, persistence, util as orm_util
|
||||
|
||||
|
||||
def track_cascade_events(descriptor, prop):
|
||||
"""Establish event listeners on object attributes which handle
|
||||
@@ -29,25 +28,42 @@ def track_cascade_events(descriptor, prop):
|
||||
# process "save_update" cascade rules for when
|
||||
# an instance is appended to the list of another instance
|
||||
|
||||
sess = session._state_session(state)
|
||||
if item is None:
|
||||
return
|
||||
|
||||
sess = state.session
|
||||
if sess:
|
||||
if sess._warn_on_events:
|
||||
sess._flush_warning("collection append")
|
||||
|
||||
prop = state.manager.mapper._props[key]
|
||||
item_state = attributes.instance_state(item)
|
||||
if prop.cascade.save_update and \
|
||||
if prop._cascade.save_update and \
|
||||
(prop.cascade_backrefs or key == initiator.key) and \
|
||||
not sess._contains_state(item_state):
|
||||
not sess._contains_state(item_state):
|
||||
sess._save_or_update_state(item_state)
|
||||
return item
|
||||
|
||||
def remove(state, item, initiator):
|
||||
sess = session._state_session(state)
|
||||
if item is None:
|
||||
return
|
||||
|
||||
sess = state.session
|
||||
if sess:
|
||||
|
||||
prop = state.manager.mapper._props[key]
|
||||
|
||||
if sess._warn_on_events:
|
||||
sess._flush_warning(
|
||||
"collection remove"
|
||||
if prop.uselist
|
||||
else "related attribute delete")
|
||||
|
||||
# expunge pending orphans
|
||||
item_state = attributes.instance_state(item)
|
||||
if prop.cascade.delete_orphan and \
|
||||
if prop._cascade.delete_orphan and \
|
||||
item_state in sess._new and \
|
||||
prop.mapper._is_orphan(item_state):
|
||||
prop.mapper._is_orphan(item_state):
|
||||
sess.expunge(item)
|
||||
|
||||
def set_(state, newvalue, oldvalue, initiator):
|
||||
@@ -56,19 +72,23 @@ def track_cascade_events(descriptor, prop):
|
||||
if oldvalue is newvalue:
|
||||
return newvalue
|
||||
|
||||
sess = session._state_session(state)
|
||||
sess = state.session
|
||||
if sess:
|
||||
|
||||
if sess._warn_on_events:
|
||||
sess._flush_warning("related attribute set")
|
||||
|
||||
prop = state.manager.mapper._props[key]
|
||||
if newvalue is not None:
|
||||
newvalue_state = attributes.instance_state(newvalue)
|
||||
if prop.cascade.save_update and \
|
||||
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:
|
||||
prop._cascade.delete_orphan:
|
||||
# possible to reach here with attributes.NEVER_SET ?
|
||||
oldvalue_state = attributes.instance_state(oldvalue)
|
||||
|
||||
@@ -154,7 +174,8 @@ class UOWTransaction(object):
|
||||
|
||||
def get_attribute_history(self, state, key,
|
||||
passive=attributes.PASSIVE_NO_INITIALIZE):
|
||||
"""facade to attributes.get_state_history(), including caching of results."""
|
||||
"""facade to attributes.get_state_history(), including
|
||||
caching of results."""
|
||||
|
||||
hashkey = ("history", state, key)
|
||||
|
||||
@@ -166,11 +187,13 @@ class UOWTransaction(object):
|
||||
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:
|
||||
|
||||
if not cached_passive & attributes.SQL_OK \
|
||||
and passive & attributes.SQL_OK:
|
||||
impl = state.manager[key].impl
|
||||
history = impl.get_history(state, state.dict,
|
||||
attributes.PASSIVE_OFF)
|
||||
attributes.PASSIVE_OFF |
|
||||
attributes.LOAD_AGAINST_COMMITTED)
|
||||
if history and impl.uses_objects:
|
||||
state_history = history.as_state()
|
||||
else:
|
||||
@@ -180,12 +203,14 @@ class UOWTransaction(object):
|
||||
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)
|
||||
history = impl.get_history(state, state.dict, passive |
|
||||
attributes.LOAD_AGAINST_COMMITTED)
|
||||
if history and impl.uses_objects:
|
||||
state_history = history.as_state()
|
||||
else:
|
||||
state_history = history
|
||||
self.attributes[hashkey] = (history, state_history, passive)
|
||||
self.attributes[hashkey] = (history, state_history,
|
||||
passive)
|
||||
|
||||
return state_history
|
||||
|
||||
@@ -204,14 +229,14 @@ class UOWTransaction(object):
|
||||
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))
|
||||
(orm_util.state_class_str(state), operation, prop))
|
||||
return False
|
||||
|
||||
if state not in self.states:
|
||||
mapper = state.manager.mapper
|
||||
|
||||
if mapper not in self.mappers:
|
||||
mapper._per_mapper_flush_actions(self)
|
||||
self._per_mapper_flush_actions(mapper)
|
||||
|
||||
self.mappers[mapper].add(state)
|
||||
self.states[state] = (isdelete, listonly)
|
||||
@@ -226,6 +251,20 @@ class UOWTransaction(object):
|
||||
states.add(state)
|
||||
cols.update(post_update_cols)
|
||||
|
||||
def _per_mapper_flush_actions(self, mapper):
|
||||
saves = SaveUpdateAll(self, mapper.base_mapper)
|
||||
deletes = DeleteAll(self, mapper.base_mapper)
|
||||
self.dependencies.add((saves, deletes))
|
||||
|
||||
for dep in mapper._dependency_processors:
|
||||
dep.per_property_preprocessors(self)
|
||||
|
||||
for prop in mapper.relationships:
|
||||
if prop.viewonly:
|
||||
continue
|
||||
dep = prop._dependency_processor
|
||||
dep.per_property_preprocessors(self)
|
||||
|
||||
@util.memoized_property
|
||||
def _mapper_for_dep(self):
|
||||
"""return a dynamic mapping of (Mapper, DependencyProcessor) to
|
||||
@@ -237,7 +276,7 @@ class UOWTransaction(object):
|
||||
|
||||
"""
|
||||
return util.PopulateDict(
|
||||
lambda tup:tup[0]._props.get(tup[1].key) is tup[1].prop
|
||||
lambda tup: tup[0]._props.get(tup[1].key) is tup[1].prop
|
||||
)
|
||||
|
||||
def filter_states_for_dep(self, dep, states):
|
||||
@@ -274,7 +313,7 @@ class UOWTransaction(object):
|
||||
# see if the graph of mapper dependencies has cycles.
|
||||
self.cycles = cycles = topological.find_cycles(
|
||||
self.dependencies,
|
||||
self.postsort_actions.values())
|
||||
list(self.postsort_actions.values()))
|
||||
|
||||
if cycles:
|
||||
# if yes, break the per-mapper actions into
|
||||
@@ -330,32 +369,36 @@ class UOWTransaction(object):
|
||||
postsort_actions):
|
||||
rec.execute(self)
|
||||
|
||||
|
||||
def finalize_flush_changes(self):
|
||||
"""mark processed objects as clean / deleted after a successful flush().
|
||||
"""mark processed objects as clean / deleted after a successful
|
||||
flush().
|
||||
|
||||
this method is called within the flush() method after the
|
||||
execute() method has succeeded and the transaction has been committed.
|
||||
|
||||
"""
|
||||
for state, (isdelete, listonly) in self.states.iteritems():
|
||||
if isdelete:
|
||||
self.session._remove_newly_deleted(state)
|
||||
else:
|
||||
# if listonly:
|
||||
# debug... would like to see how many do this
|
||||
self.session._register_newly_persistent(state)
|
||||
states = set(self.states)
|
||||
isdel = set(
|
||||
s for (s, (isdelete, listonly)) in self.states.items()
|
||||
if isdelete
|
||||
)
|
||||
other = states.difference(isdel)
|
||||
self.session._remove_newly_deleted(isdel)
|
||||
self.session._register_newly_persistent(other)
|
||||
|
||||
|
||||
class IterateMappersMixin(object):
|
||||
def _mappers(self, uow):
|
||||
if self.fromparent:
|
||||
return iter(
|
||||
m for m in self.dependency_processor.parent.self_and_descendants
|
||||
m for m in
|
||||
self.dependency_processor.parent.self_and_descendants
|
||||
if uow._mapper_for_dep[(m, self.dependency_processor)]
|
||||
)
|
||||
else:
|
||||
return self.dependency_processor.mapper.self_and_descendants
|
||||
|
||||
|
||||
class Preprocess(IterateMappersMixin):
|
||||
def __init__(self, dependency_processor, fromparent):
|
||||
self.dependency_processor = dependency_processor
|
||||
@@ -396,6 +439,7 @@ class Preprocess(IterateMappersMixin):
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class PostSortRec(object):
|
||||
disabled = False
|
||||
|
||||
@@ -418,12 +462,14 @@ class PostSortRec(object):
|
||||
",".join(str(x) for x in self.__dict__.values())
|
||||
)
|
||||
|
||||
|
||||
class ProcessAll(IterateMappersMixin, PostSortRec):
|
||||
def __init__(self, uow, dependency_processor, delete, fromparent):
|
||||
self.dependency_processor = dependency_processor
|
||||
self.delete = delete
|
||||
self.fromparent = fromparent
|
||||
uow.deps[dependency_processor.parent.base_mapper].add(dependency_processor)
|
||||
uow.deps[dependency_processor.parent.base_mapper].\
|
||||
add(dependency_processor)
|
||||
|
||||
def execute(self, uow):
|
||||
states = self._elements(uow)
|
||||
@@ -453,6 +499,7 @@ class ProcessAll(IterateMappersMixin, PostSortRec):
|
||||
if isdelete == self.delete and not listonly:
|
||||
yield state
|
||||
|
||||
|
||||
class IssuePostUpdate(PostSortRec):
|
||||
def __init__(self, uow, mapper, isdelete):
|
||||
self.mapper = mapper
|
||||
@@ -464,6 +511,7 @@ class IssuePostUpdate(PostSortRec):
|
||||
|
||||
persistence.post_update(self.mapper, states, uow, cols)
|
||||
|
||||
|
||||
class SaveUpdateAll(PostSortRec):
|
||||
def __init__(self, uow, mapper):
|
||||
self.mapper = mapper
|
||||
@@ -476,17 +524,22 @@ class SaveUpdateAll(PostSortRec):
|
||||
)
|
||||
|
||||
def per_state_flush_actions(self, uow):
|
||||
states = list(uow.states_for_mapper_hierarchy(self.mapper, False, False))
|
||||
for rec in self.mapper._per_state_flush_actions(
|
||||
uow,
|
||||
states,
|
||||
False):
|
||||
yield rec
|
||||
states = list(uow.states_for_mapper_hierarchy(
|
||||
self.mapper, False, False))
|
||||
base_mapper = self.mapper.base_mapper
|
||||
delete_all = DeleteAll(uow, base_mapper)
|
||||
for state in states:
|
||||
# keep saves before deletes -
|
||||
# this ensures 'row switch' operations work
|
||||
action = SaveUpdateState(uow, state, base_mapper)
|
||||
uow.dependencies.add((action, delete_all))
|
||||
yield action
|
||||
|
||||
for dep in uow.deps[self.mapper]:
|
||||
states_for_prop = uow.filter_states_for_dep(dep, states)
|
||||
dep.per_state_flush_actions(uow, states_for_prop, False)
|
||||
|
||||
|
||||
class DeleteAll(PostSortRec):
|
||||
def __init__(self, uow, mapper):
|
||||
self.mapper = mapper
|
||||
@@ -499,17 +552,22 @@ class DeleteAll(PostSortRec):
|
||||
)
|
||||
|
||||
def per_state_flush_actions(self, uow):
|
||||
states = list(uow.states_for_mapper_hierarchy(self.mapper, True, False))
|
||||
for rec in self.mapper._per_state_flush_actions(
|
||||
uow,
|
||||
states,
|
||||
True):
|
||||
yield rec
|
||||
states = list(uow.states_for_mapper_hierarchy(
|
||||
self.mapper, True, False))
|
||||
base_mapper = self.mapper.base_mapper
|
||||
save_all = SaveUpdateAll(uow, base_mapper)
|
||||
for state in states:
|
||||
# keep saves before deletes -
|
||||
# this ensures 'row switch' operations work
|
||||
action = DeleteState(uow, state, base_mapper)
|
||||
uow.dependencies.add((save_all, action))
|
||||
yield action
|
||||
|
||||
for dep in uow.deps[self.mapper]:
|
||||
states_for_prop = uow.filter_states_for_dep(dep, states)
|
||||
dep.per_state_flush_actions(uow, states_for_prop, True)
|
||||
|
||||
|
||||
class ProcessState(PostSortRec):
|
||||
def __init__(self, uow, dependency_processor, delete, state):
|
||||
self.dependency_processor = dependency_processor
|
||||
@@ -535,10 +593,11 @@ class ProcessState(PostSortRec):
|
||||
return "%s(%s, %s, delete=%s)" % (
|
||||
self.__class__.__name__,
|
||||
self.dependency_processor,
|
||||
mapperutil.state_str(self.state),
|
||||
orm_util.state_str(self.state),
|
||||
self.delete
|
||||
)
|
||||
|
||||
|
||||
class SaveUpdateState(PostSortRec):
|
||||
def __init__(self, uow, state, mapper):
|
||||
self.state = state
|
||||
@@ -559,9 +618,10 @@ class SaveUpdateState(PostSortRec):
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (
|
||||
self.__class__.__name__,
|
||||
mapperutil.state_str(self.state)
|
||||
orm_util.state_str(self.state)
|
||||
)
|
||||
|
||||
|
||||
class DeleteState(PostSortRec):
|
||||
def __init__(self, uow, state, mapper):
|
||||
self.state = state
|
||||
@@ -582,6 +642,5 @@ class DeleteState(PostSortRec):
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (
|
||||
self.__class__.__name__,
|
||||
mapperutil.state_str(self.state)
|
||||
orm_util.state_str(self.state)
|
||||
)
|
||||
|
||||
|
||||
+597
-394
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user