Move vendored libraries to vendor directory
This commit is contained in:
Vendored
+1807
File diff suppressed because it is too large
Load Diff
Vendored
+1558
File diff suppressed because it is too large
Load Diff
Vendored
+1563
File diff suppressed because it is too large
Load Diff
Vendored
+1165
File diff suppressed because it is too large
Load Diff
+588
@@ -0,0 +1,588 @@
|
||||
# orm/deprecated_interfaces.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 .. import event, util
|
||||
from .interfaces import EXT_CONTINUE
|
||||
|
||||
|
||||
class MapperExtension(object):
|
||||
"""Base implementation for :class:`.Mapper` event hooks.
|
||||
|
||||
.. note::
|
||||
|
||||
:class:`.MapperExtension` is deprecated. Please
|
||||
refer to :func:`.event.listen` as well as
|
||||
:class:`.MapperEvents`.
|
||||
|
||||
New extension classes subclass :class:`.MapperExtension` and are specified
|
||||
using the ``extension`` mapper() argument, which is a single
|
||||
:class:`.MapperExtension` or a list of such::
|
||||
|
||||
from sqlalchemy.orm.interfaces import MapperExtension
|
||||
|
||||
class MyExtension(MapperExtension):
|
||||
def before_insert(self, mapper, connection, instance):
|
||||
print "instance %s before insert !" % instance
|
||||
|
||||
m = mapper(User, users_table, extension=MyExtension())
|
||||
|
||||
A single mapper can maintain a chain of ``MapperExtension``
|
||||
objects. When a particular mapping event occurs, the
|
||||
corresponding method on each ``MapperExtension`` is invoked
|
||||
serially, and each method has the ability to halt the chain
|
||||
from proceeding further::
|
||||
|
||||
m = mapper(User, users_table, extension=[ext1, ext2, ext3])
|
||||
|
||||
Each ``MapperExtension`` method returns the symbol
|
||||
EXT_CONTINUE by default. This symbol generally means "move
|
||||
to the next ``MapperExtension`` for processing". For methods
|
||||
that return objects like translated rows or new object
|
||||
instances, EXT_CONTINUE means the result of the method
|
||||
should be ignored. In some cases it's required for a
|
||||
default mapper activity to be performed, such as adding a
|
||||
new instance to a result list.
|
||||
|
||||
The symbol EXT_STOP has significance within a chain
|
||||
of ``MapperExtension`` objects that the chain will be stopped
|
||||
when this symbol is returned. Like EXT_CONTINUE, it also
|
||||
has additional significance in some cases that a default
|
||||
mapper activity will not be performed.
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _adapt_instrument_class(cls, self, listener):
|
||||
cls._adapt_listener_methods(self, listener, ('instrument_class',))
|
||||
|
||||
@classmethod
|
||||
def _adapt_listener(cls, self, listener):
|
||||
cls._adapt_listener_methods(
|
||||
self, listener,
|
||||
(
|
||||
'init_instance',
|
||||
'init_failed',
|
||||
'translate_row',
|
||||
'create_instance',
|
||||
'append_result',
|
||||
'populate_instance',
|
||||
'reconstruct_instance',
|
||||
'before_insert',
|
||||
'after_insert',
|
||||
'before_update',
|
||||
'after_update',
|
||||
'before_delete',
|
||||
'after_delete'
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def _adapt_listener_methods(cls, self, listener, methods):
|
||||
|
||||
for meth in methods:
|
||||
me_meth = getattr(MapperExtension, meth)
|
||||
ls_meth = getattr(listener, meth)
|
||||
|
||||
if not util.methods_equivalent(me_meth, ls_meth):
|
||||
if meth == 'reconstruct_instance':
|
||||
def go(ls_meth):
|
||||
def reconstruct(instance, ctx):
|
||||
ls_meth(self, instance)
|
||||
return reconstruct
|
||||
event.listen(self.class_manager, 'load',
|
||||
go(ls_meth), raw=False, propagate=True)
|
||||
elif meth == 'init_instance':
|
||||
def go(ls_meth):
|
||||
def init_instance(instance, args, kwargs):
|
||||
ls_meth(self, self.class_,
|
||||
self.class_manager.original_init,
|
||||
instance, args, kwargs)
|
||||
return init_instance
|
||||
event.listen(self.class_manager, 'init',
|
||||
go(ls_meth), raw=False, propagate=True)
|
||||
elif meth == 'init_failed':
|
||||
def go(ls_meth):
|
||||
def init_failed(instance, args, kwargs):
|
||||
util.warn_exception(ls_meth, self, self.class_,
|
||||
self.class_manager.original_init,
|
||||
instance, args, kwargs)
|
||||
|
||||
return init_failed
|
||||
event.listen(self.class_manager, 'init_failure',
|
||||
go(ls_meth), raw=False, propagate=True)
|
||||
else:
|
||||
event.listen(self, "%s" % meth, ls_meth,
|
||||
raw=False, retval=True, propagate=True)
|
||||
|
||||
def instrument_class(self, mapper, class_):
|
||||
"""Receive a class when the mapper is first constructed, and has
|
||||
applied instrumentation to the mapped class.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def init_instance(self, mapper, class_, oldinit, instance, args, kwargs):
|
||||
"""Receive an instance when it's constructor is called.
|
||||
|
||||
This method is only called during a userland construction of
|
||||
an object. It is not called when an object is loaded from the
|
||||
database.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def init_failed(self, mapper, class_, oldinit, instance, args, kwargs):
|
||||
"""Receive an instance when it's constructor has been called,
|
||||
and raised an exception.
|
||||
|
||||
This method is only called during a userland construction of
|
||||
an object. It is not called when an object is loaded from the
|
||||
database.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def translate_row(self, mapper, context, row):
|
||||
"""Perform pre-processing on the given result row and return a
|
||||
new row instance.
|
||||
|
||||
This is called when the mapper first receives a row, before
|
||||
the object identity or the instance itself has been derived
|
||||
from that row. The given row may or may not be a
|
||||
``RowProxy`` object - it will always be a dictionary-like
|
||||
object which contains mapped columns as keys. The
|
||||
returned object should also be a dictionary-like object
|
||||
which recognizes mapped columns as keys.
|
||||
|
||||
If the ultimate return value is EXT_CONTINUE, the row
|
||||
is not translated.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def create_instance(self, mapper, selectcontext, row, class_):
|
||||
"""Receive a row when a new object instance is about to be
|
||||
created from that row.
|
||||
|
||||
The method can choose to create the instance itself, or it can return
|
||||
EXT_CONTINUE to indicate normal object creation should take place.
|
||||
|
||||
mapper
|
||||
The mapper doing the operation
|
||||
|
||||
selectcontext
|
||||
The QueryContext generated from the Query.
|
||||
|
||||
row
|
||||
The result row from the database
|
||||
|
||||
class\_
|
||||
The class we are mapping.
|
||||
|
||||
return value
|
||||
A new object instance, or EXT_CONTINUE
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def append_result(self, mapper, selectcontext, row, instance,
|
||||
result, **flags):
|
||||
"""Receive an object instance before that instance is appended
|
||||
to a result list.
|
||||
|
||||
If this method returns EXT_CONTINUE, result appending will proceed
|
||||
normally. if this method returns any other value or None,
|
||||
result appending will not proceed for this instance, giving
|
||||
this extension an opportunity to do the appending itself, if
|
||||
desired.
|
||||
|
||||
mapper
|
||||
The mapper doing the operation.
|
||||
|
||||
selectcontext
|
||||
The QueryContext generated from the Query.
|
||||
|
||||
row
|
||||
The result row from the database.
|
||||
|
||||
instance
|
||||
The object instance to be appended to the result.
|
||||
|
||||
result
|
||||
List to which results are being appended.
|
||||
|
||||
\**flags
|
||||
extra information about the row, same as criterion in
|
||||
``create_row_processor()`` method of
|
||||
:class:`~sqlalchemy.orm.interfaces.MapperProperty`
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def populate_instance(self, mapper, selectcontext, row,
|
||||
instance, **flags):
|
||||
"""Receive an instance before that instance has
|
||||
its attributes populated.
|
||||
|
||||
This usually corresponds to a newly loaded instance but may
|
||||
also correspond to an already-loaded instance which has
|
||||
unloaded attributes to be populated. The method may be called
|
||||
many times for a single instance, as multiple result rows are
|
||||
used to populate eagerly loaded collections.
|
||||
|
||||
If this method returns EXT_CONTINUE, instance population will
|
||||
proceed normally. If any other value or None is returned,
|
||||
instance population will not proceed, giving this extension an
|
||||
opportunity to populate the instance itself, if desired.
|
||||
|
||||
.. deprecated:: 0.5
|
||||
Most usages of this hook are obsolete. For a
|
||||
generic "object has been newly created from a row" hook, use
|
||||
``reconstruct_instance()``, or the ``@orm.reconstructor``
|
||||
decorator.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def reconstruct_instance(self, mapper, instance):
|
||||
"""Receive an object instance after it has been created via
|
||||
``__new__``, and after initial attribute population has
|
||||
occurred.
|
||||
|
||||
This typically occurs when the instance is created based on
|
||||
incoming result rows, and is only called once for that
|
||||
instance's lifetime.
|
||||
|
||||
Note that during a result-row load, this method is called upon
|
||||
the first row received for this instance. Note that some
|
||||
attributes and collections may or may not be loaded or even
|
||||
initialized, depending on what's present in the result rows.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
return EXT_CONTINUE
|
||||
|
||||
def before_insert(self, mapper, connection, instance):
|
||||
"""Receive an object instance before that instance is inserted
|
||||
into its table.
|
||||
|
||||
This is a good place to set up primary key values and such
|
||||
that aren't handled otherwise.
|
||||
|
||||
Column-based attributes can be modified within this method
|
||||
which will result in the new value being inserted. However
|
||||
*no* changes to the overall flush plan can be made, and
|
||||
manipulation of the ``Session`` will not have the desired effect.
|
||||
To manipulate the ``Session`` within an extension, use
|
||||
``SessionExtension``.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def after_insert(self, mapper, connection, instance):
|
||||
"""Receive an object instance after that instance is inserted.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def before_update(self, mapper, connection, instance):
|
||||
"""Receive an object instance before that instance is updated.
|
||||
|
||||
Note that this method is called for all instances that are marked as
|
||||
"dirty", even those which have no net changes to their column-based
|
||||
attributes. An object is marked as dirty when any of its column-based
|
||||
attributes have a "set attribute" operation called or when any of its
|
||||
collections are modified. If, at update time, no column-based
|
||||
attributes have any net changes, no UPDATE statement will be issued.
|
||||
This means that an instance being sent to before_update is *not* a
|
||||
guarantee that an UPDATE statement will be issued (although you can
|
||||
affect the outcome here).
|
||||
|
||||
To detect if the column-based attributes on the object have net
|
||||
changes, and will therefore generate an UPDATE statement, use
|
||||
``object_session(instance).is_modified(instance,
|
||||
include_collections=False)``.
|
||||
|
||||
Column-based attributes can be modified within this method
|
||||
which will result in the new value being updated. However
|
||||
*no* changes to the overall flush plan can be made, and
|
||||
manipulation of the ``Session`` will not have the desired effect.
|
||||
To manipulate the ``Session`` within an extension, use
|
||||
``SessionExtension``.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def after_update(self, mapper, connection, instance):
|
||||
"""Receive an object instance after that instance is updated.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def before_delete(self, mapper, connection, instance):
|
||||
"""Receive an object instance before that instance is deleted.
|
||||
|
||||
Note that *no* changes to the overall flush plan can be made
|
||||
here; and manipulation of the ``Session`` will not have the
|
||||
desired effect. To manipulate the ``Session`` within an
|
||||
extension, use ``SessionExtension``.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
def after_delete(self, mapper, connection, instance):
|
||||
"""Receive an object instance after that instance is deleted.
|
||||
|
||||
The return value is only significant within the ``MapperExtension``
|
||||
chain; the parent mapper's behavior isn't modified by this method.
|
||||
|
||||
"""
|
||||
|
||||
return EXT_CONTINUE
|
||||
|
||||
|
||||
class SessionExtension(object):
|
||||
|
||||
"""Base implementation for :class:`.Session` event hooks.
|
||||
|
||||
.. note::
|
||||
|
||||
:class:`.SessionExtension` is deprecated. Please
|
||||
refer to :func:`.event.listen` as well as
|
||||
:class:`.SessionEvents`.
|
||||
|
||||
Subclasses may be installed into a :class:`.Session` (or
|
||||
:class:`.sessionmaker`) using the ``extension`` keyword
|
||||
argument::
|
||||
|
||||
from sqlalchemy.orm.interfaces import SessionExtension
|
||||
|
||||
class MySessionExtension(SessionExtension):
|
||||
def before_commit(self, session):
|
||||
print "before commit!"
|
||||
|
||||
Session = sessionmaker(extension=MySessionExtension())
|
||||
|
||||
The same :class:`.SessionExtension` instance can be used
|
||||
with any number of sessions.
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _adapt_listener(cls, self, listener):
|
||||
for meth in [
|
||||
'before_commit',
|
||||
'after_commit',
|
||||
'after_rollback',
|
||||
'before_flush',
|
||||
'after_flush',
|
||||
'after_flush_postexec',
|
||||
'after_begin',
|
||||
'after_attach',
|
||||
'after_bulk_update',
|
||||
'after_bulk_delete',
|
||||
]:
|
||||
me_meth = getattr(SessionExtension, meth)
|
||||
ls_meth = getattr(listener, meth)
|
||||
|
||||
if not util.methods_equivalent(me_meth, ls_meth):
|
||||
event.listen(self, meth, getattr(listener, meth))
|
||||
|
||||
def before_commit(self, session):
|
||||
"""Execute right before commit is called.
|
||||
|
||||
Note that this may not be per-flush if a longer running
|
||||
transaction is ongoing."""
|
||||
|
||||
def after_commit(self, session):
|
||||
"""Execute after a commit has occurred.
|
||||
|
||||
Note that this may not be per-flush if a longer running
|
||||
transaction is ongoing."""
|
||||
|
||||
def after_rollback(self, session):
|
||||
"""Execute after a rollback has occurred.
|
||||
|
||||
Note that this may not be per-flush if a longer running
|
||||
transaction is ongoing."""
|
||||
|
||||
def before_flush(self, session, flush_context, instances):
|
||||
"""Execute before flush process has started.
|
||||
|
||||
`instances` is an optional list of objects which were passed to
|
||||
the ``flush()`` method. """
|
||||
|
||||
def after_flush(self, session, flush_context):
|
||||
"""Execute after flush has completed, but before commit has been
|
||||
called.
|
||||
|
||||
Note that the session's state is still in pre-flush, i.e. 'new',
|
||||
'dirty', and 'deleted' lists still show pre-flush state as well
|
||||
as the history settings on instance attributes."""
|
||||
|
||||
def after_flush_postexec(self, session, flush_context):
|
||||
"""Execute after flush has completed, and after the post-exec
|
||||
state occurs.
|
||||
|
||||
This will be when the 'new', 'dirty', and 'deleted' lists are in
|
||||
their final state. An actual commit() may or may not have
|
||||
occurred, depending on whether or not the flush started its own
|
||||
transaction or participated in a larger transaction. """
|
||||
|
||||
def after_begin(self, session, transaction, connection):
|
||||
"""Execute after a transaction is begun on a connection
|
||||
|
||||
`transaction` is the SessionTransaction. This method is called
|
||||
after an engine level transaction is begun on a connection. """
|
||||
|
||||
def after_attach(self, session, instance):
|
||||
"""Execute after an instance is attached to a session.
|
||||
|
||||
This is called after an add, delete or merge. """
|
||||
|
||||
def after_bulk_update(self, session, query, query_context, result):
|
||||
"""Execute after a bulk update operation to the session.
|
||||
|
||||
This is called after a session.query(...).update()
|
||||
|
||||
`query` is the query object that this update operation was
|
||||
called on. `query_context` was the query context object.
|
||||
`result` is the result object returned from the bulk operation.
|
||||
"""
|
||||
|
||||
def after_bulk_delete(self, session, query, query_context, result):
|
||||
"""Execute after a bulk delete operation to the session.
|
||||
|
||||
This is called after a session.query(...).delete()
|
||||
|
||||
`query` is the query object that this delete operation was
|
||||
called on. `query_context` was the query context object.
|
||||
`result` is the result object returned from the bulk operation.
|
||||
"""
|
||||
|
||||
|
||||
class AttributeExtension(object):
|
||||
"""Base implementation for :class:`.AttributeImpl` event hooks, events
|
||||
that fire upon attribute mutations in user code.
|
||||
|
||||
.. note::
|
||||
|
||||
:class:`.AttributeExtension` is deprecated. Please
|
||||
refer to :func:`.event.listen` as well as
|
||||
:class:`.AttributeEvents`.
|
||||
|
||||
:class:`.AttributeExtension` is used to listen for set,
|
||||
remove, and append events on individual mapped attributes.
|
||||
It is established on an individual mapped attribute using
|
||||
the `extension` argument, available on
|
||||
:func:`.column_property`, :func:`.relationship`, and
|
||||
others::
|
||||
|
||||
from sqlalchemy.orm.interfaces import AttributeExtension
|
||||
from sqlalchemy.orm import mapper, relationship, column_property
|
||||
|
||||
class MyAttrExt(AttributeExtension):
|
||||
def append(self, state, value, initiator):
|
||||
print "append event !"
|
||||
return value
|
||||
|
||||
def set(self, state, value, oldvalue, initiator):
|
||||
print "set event !"
|
||||
return value
|
||||
|
||||
mapper(SomeClass, sometable, properties={
|
||||
'foo':column_property(sometable.c.foo, extension=MyAttrExt()),
|
||||
'bar':relationship(Bar, extension=MyAttrExt())
|
||||
})
|
||||
|
||||
Note that the :class:`.AttributeExtension` methods
|
||||
:meth:`~.AttributeExtension.append` and
|
||||
:meth:`~.AttributeExtension.set` need to return the
|
||||
``value`` parameter. The returned value is used as the
|
||||
effective value, and allows the extension to change what is
|
||||
ultimately persisted.
|
||||
|
||||
AttributeExtension is assembled within the descriptors associated
|
||||
with a mapped class.
|
||||
|
||||
"""
|
||||
|
||||
active_history = True
|
||||
"""indicates that the set() method would like to receive the 'old' value,
|
||||
even if it means firing lazy callables.
|
||||
|
||||
Note that ``active_history`` can also be set directly via
|
||||
:func:`.column_property` and :func:`.relationship`.
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _adapt_listener(cls, self, listener):
|
||||
event.listen(self, 'append', listener.append,
|
||||
active_history=listener.active_history,
|
||||
raw=True, retval=True)
|
||||
event.listen(self, 'remove', listener.remove,
|
||||
active_history=listener.active_history,
|
||||
raw=True, retval=True)
|
||||
event.listen(self, 'set', listener.set,
|
||||
active_history=listener.active_history,
|
||||
raw=True, retval=True)
|
||||
|
||||
def append(self, state, value, initiator):
|
||||
"""Receive a collection append event.
|
||||
|
||||
The returned value will be used as the actual value to be
|
||||
appended.
|
||||
|
||||
"""
|
||||
return value
|
||||
|
||||
def remove(self, state, value, initiator):
|
||||
"""Receive a remove event.
|
||||
|
||||
No return value is defined.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def set(self, state, value, oldvalue, initiator):
|
||||
"""Receive a set event.
|
||||
|
||||
The returned value will be used as the actual value to be
|
||||
set.
|
||||
|
||||
"""
|
||||
return value
|
||||
+478
@@ -0,0 +1,478 @@
|
||||
# orm/descriptor_props.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
|
||||
|
||||
"""Descriptor properties are more "auxiliary" properties
|
||||
that exist as configurational elements, but don't participate
|
||||
as actively in the load/persist ORM loop.
|
||||
|
||||
"""
|
||||
|
||||
from .interfaces import MapperProperty, PropComparator
|
||||
from .util import _none_set
|
||||
from . import attributes, strategies
|
||||
from .. import util, sql, exc as sa_exc, event, schema
|
||||
from ..sql import expression
|
||||
properties = util.importlater('sqlalchemy.orm', 'properties')
|
||||
|
||||
|
||||
class DescriptorProperty(MapperProperty):
|
||||
""":class:`.MapperProperty` which proxies access to a
|
||||
user-defined descriptor."""
|
||||
|
||||
doc = None
|
||||
|
||||
def instrument_class(self, mapper):
|
||||
prop = self
|
||||
|
||||
class _ProxyImpl(object):
|
||||
accepts_scalar_loader = False
|
||||
expire_missing = True
|
||||
collection = False
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
if hasattr(prop, 'get_history'):
|
||||
def get_history(self, state, dict_,
|
||||
passive=attributes.PASSIVE_OFF):
|
||||
return prop.get_history(state, dict_, passive)
|
||||
|
||||
if self.descriptor is None:
|
||||
desc = getattr(mapper.class_, self.key, None)
|
||||
if mapper._is_userland_descriptor(desc):
|
||||
self.descriptor = desc
|
||||
|
||||
if self.descriptor is None:
|
||||
def fset(obj, value):
|
||||
setattr(obj, self.name, value)
|
||||
|
||||
def fdel(obj):
|
||||
delattr(obj, self.name)
|
||||
|
||||
def fget(obj):
|
||||
return getattr(obj, self.name)
|
||||
|
||||
self.descriptor = property(
|
||||
fget=fget,
|
||||
fset=fset,
|
||||
fdel=fdel,
|
||||
)
|
||||
|
||||
proxy_attr = attributes.\
|
||||
create_proxied_attribute(self.descriptor)\
|
||||
(
|
||||
self.parent.class_,
|
||||
self.key,
|
||||
self.descriptor,
|
||||
lambda: self._comparator_factory(mapper),
|
||||
doc=self.doc,
|
||||
original_property=self
|
||||
)
|
||||
proxy_attr.impl = _ProxyImpl(self.key)
|
||||
mapper.class_manager.instrument_attribute(self.key, proxy_attr)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
See also:
|
||||
|
||||
:ref:`mapper_composite`
|
||||
|
||||
"""
|
||||
def __init__(self, class_, *attrs, **kwargs):
|
||||
self.attrs = attrs
|
||||
self.composite_class = class_
|
||||
self.active_history = kwargs.get('active_history', False)
|
||||
self.deferred = kwargs.get('deferred', False)
|
||||
self.group = kwargs.get('group', None)
|
||||
self.comparator_factory = kwargs.pop('comparator_factory',
|
||||
self.__class__.Comparator)
|
||||
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()
|
||||
|
||||
def do_init(self):
|
||||
"""Initialization which occurs after the :class:`.CompositeProperty`
|
||||
has been associated with its parent mapper.
|
||||
|
||||
"""
|
||||
self._init_props()
|
||||
self._setup_arguments_on_columns()
|
||||
|
||||
def _create_descriptor(self):
|
||||
"""Create the Python descriptor that will serve as
|
||||
the access point on instances of the mapped class.
|
||||
|
||||
"""
|
||||
|
||||
def fget(instance):
|
||||
dict_ = attributes.instance_dict(instance)
|
||||
state = attributes.instance_state(instance)
|
||||
|
||||
if self.key not in dict_:
|
||||
# key not present. Iterate through related
|
||||
# attributes, retrieve their values. This
|
||||
# ensures they all load.
|
||||
values = [
|
||||
getattr(instance, key)
|
||||
for key in self._attribute_keys
|
||||
]
|
||||
|
||||
# current expected behavior here is that the composite is
|
||||
# created on access if the object is persistent or if
|
||||
# col attributes have non-None. This would be better
|
||||
# if the composite were created unconditionally,
|
||||
# but that would be a behavioral change.
|
||||
if self.key not in dict_ and (
|
||||
state.key is not None or
|
||||
not _none_set.issuperset(values)
|
||||
):
|
||||
dict_[self.key] = self.composite_class(*values)
|
||||
state.manager.dispatch.refresh(state, None, [self.key])
|
||||
|
||||
return dict_.get(self.key, None)
|
||||
|
||||
def fset(instance, value):
|
||||
dict_ = attributes.instance_dict(instance)
|
||||
state = attributes.instance_state(instance)
|
||||
attr = state.manager[self.key]
|
||||
previous = dict_.get(self.key, attributes.NO_VALUE)
|
||||
for fn in attr.dispatch.set:
|
||||
value = fn(state, value, previous, attr.impl)
|
||||
dict_[self.key] = value
|
||||
if value is None:
|
||||
for key in self._attribute_keys:
|
||||
setattr(instance, key, None)
|
||||
else:
|
||||
for key, value in zip(
|
||||
self._attribute_keys,
|
||||
value.__composite_values__()):
|
||||
setattr(instance, key, value)
|
||||
|
||||
def fdel(instance):
|
||||
state = attributes.instance_state(instance)
|
||||
dict_ = attributes.instance_dict(instance)
|
||||
previous = dict_.pop(self.key, attributes.NO_VALUE)
|
||||
attr = state.manager[self.key]
|
||||
attr.dispatch.remove(state, previous, attr.impl)
|
||||
for key in self._attribute_keys:
|
||||
setattr(instance, key, None)
|
||||
|
||||
self.descriptor = property(fget, fset, fdel)
|
||||
|
||||
@util.memoized_property
|
||||
def _comparable_elements(self):
|
||||
return [
|
||||
getattr(self.parent.class_, prop.key)
|
||||
for prop in self.props
|
||||
]
|
||||
|
||||
def _init_props(self):
|
||||
self.props = props = []
|
||||
for attr in self.attrs:
|
||||
if isinstance(attr, basestring):
|
||||
prop = self.parent.get_property(attr)
|
||||
elif isinstance(attr, schema.Column):
|
||||
prop = self.parent._columntoproperty[attr]
|
||||
elif isinstance(attr, attributes.InstrumentedAttribute):
|
||||
prop = attr.property
|
||||
props.append(prop)
|
||||
|
||||
@property
|
||||
def columns(self):
|
||||
return [a for a in self.attrs if isinstance(a, schema.Column)]
|
||||
|
||||
def _setup_arguments_on_columns(self):
|
||||
"""Propagate configuration arguments made on this composite
|
||||
to the target columns, for those that apply.
|
||||
|
||||
"""
|
||||
for prop in self.props:
|
||||
prop.active_history = self.active_history
|
||||
if self.deferred:
|
||||
prop.deferred = self.deferred
|
||||
prop.strategy_class = strategies.DeferredColumnLoader
|
||||
prop.group = self.group
|
||||
|
||||
def _setup_event_handlers(self):
|
||||
"""Establish events that populate/expire the composite attribute."""
|
||||
|
||||
def load_handler(state, *args):
|
||||
dict_ = state.dict
|
||||
|
||||
if self.key in dict_:
|
||||
return
|
||||
|
||||
# if column elements aren't loaded, skip.
|
||||
# __get__() will initiate a load for those
|
||||
# columns
|
||||
for k in self._attribute_keys:
|
||||
if k not in dict_:
|
||||
return
|
||||
|
||||
#assert self.key not in dict_
|
||||
dict_[self.key] = self.composite_class(
|
||||
*[state.dict[key] for key in
|
||||
self._attribute_keys]
|
||||
)
|
||||
|
||||
def expire_handler(state, keys):
|
||||
if keys is None or set(self._attribute_keys).intersection(keys):
|
||||
state.dict.pop(self.key, None)
|
||||
|
||||
def insert_update_handler(mapper, connection, state):
|
||||
"""After an insert or update, some columns may be expired due
|
||||
to server side defaults, or re-populated due to client side
|
||||
defaults. Pop out the composite value here so that it
|
||||
recreates.
|
||||
|
||||
"""
|
||||
|
||||
state.dict.pop(self.key, None)
|
||||
|
||||
event.listen(self.parent, 'after_insert',
|
||||
insert_update_handler, raw=True)
|
||||
event.listen(self.parent, 'after_update',
|
||||
insert_update_handler, raw=True)
|
||||
event.listen(self.parent, 'load',
|
||||
load_handler, raw=True, propagate=True)
|
||||
event.listen(self.parent, 'refresh',
|
||||
load_handler, raw=True, propagate=True)
|
||||
event.listen(self.parent, 'expire',
|
||||
expire_handler, raw=True, propagate=True)
|
||||
|
||||
# TODO: need a deserialize hook here
|
||||
|
||||
@util.memoized_property
|
||||
def _attribute_keys(self):
|
||||
return [
|
||||
prop.key for prop in self.props
|
||||
]
|
||||
|
||||
def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF):
|
||||
"""Provided for userland code that uses attributes.get_history()."""
|
||||
|
||||
added = []
|
||||
deleted = []
|
||||
|
||||
has_history = False
|
||||
for prop in self.props:
|
||||
key = prop.key
|
||||
hist = state.manager[key].impl.get_history(state, dict_)
|
||||
if hist.has_changes():
|
||||
has_history = True
|
||||
|
||||
non_deleted = hist.non_deleted()
|
||||
if non_deleted:
|
||||
added.extend(non_deleted)
|
||||
else:
|
||||
added.append(None)
|
||||
if hist.deleted:
|
||||
deleted.extend(hist.deleted)
|
||||
else:
|
||||
deleted.append(None)
|
||||
|
||||
if has_history:
|
||||
return attributes.History(
|
||||
[self.composite_class(*added)],
|
||||
(),
|
||||
[self.composite_class(*deleted)]
|
||||
)
|
||||
else:
|
||||
return attributes.History(
|
||||
(), [self.composite_class(*added)], ()
|
||||
)
|
||||
|
||||
def _comparator_factory(self, mapper):
|
||||
return self.comparator_factory(self, mapper)
|
||||
|
||||
class Comparator(PropComparator):
|
||||
"""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):
|
||||
return expression.ClauseList(group=False, *self._comparable_elements)
|
||||
|
||||
__hash__ = None
|
||||
|
||||
@util.memoized_property
|
||||
def _comparable_elements(self):
|
||||
if self.adapter:
|
||||
# we need to do a little fudging here because
|
||||
# the adapter function we're given only accepts
|
||||
# ColumnElements, but our prop._comparable_elements is returning
|
||||
# InstrumentedAttribute, because we support the use case
|
||||
# of composites that refer to relationships. The better
|
||||
# solution here is to open up how AliasedClass interacts
|
||||
# with PropComparators so more context is available.
|
||||
return [self.adapter(x.__clause_element__())
|
||||
for x 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__()
|
||||
comparisons = [
|
||||
a == b
|
||||
for a, b in zip(self.prop._comparable_elements, values)
|
||||
]
|
||||
if self.adapter:
|
||||
comparisons = [self.adapter(x) for x in comparisons]
|
||||
return sql.and_(*comparisons)
|
||||
|
||||
def __ne__(self, other):
|
||||
return sql.not_(self.__eq__(other))
|
||||
|
||||
def __str__(self):
|
||||
return str(self.parent.class_.__name__) + "." + self.key
|
||||
|
||||
|
||||
class ConcreteInheritedProperty(DescriptorProperty):
|
||||
"""A 'do nothing' :class:`.MapperProperty` that disables
|
||||
an attribute on a concrete subclass that is only present
|
||||
on the inherited mapper, not the concrete classes' mapper.
|
||||
|
||||
Cases where this occurs include:
|
||||
|
||||
* When the superclass mapper is mapped against a
|
||||
"polymorphic union", which includes all attributes from
|
||||
all subclasses.
|
||||
* When a relationship() is configured on an inherited mapper,
|
||||
but not on the subclass mapper. Concrete mappers require
|
||||
that relationship() is configured explicitly on each
|
||||
subclass.
|
||||
|
||||
"""
|
||||
|
||||
def _comparator_factory(self, mapper):
|
||||
comparator_callable = None
|
||||
|
||||
for m in self.parent.iterate_to_root():
|
||||
p = m._props[self.key]
|
||||
if not isinstance(p, ConcreteInheritedProperty):
|
||||
comparator_callable = p.comparator_factory
|
||||
break
|
||||
return comparator_callable
|
||||
|
||||
def __init__(self):
|
||||
def warn():
|
||||
raise AttributeError("Concrete %s does not implement "
|
||||
"attribute %r at the instance level. Add this "
|
||||
"property explicitly to %s." %
|
||||
(self.parent, self.key, self.parent))
|
||||
|
||||
class NoninheritedConcreteProp(object):
|
||||
def __set__(s, obj, value):
|
||||
warn()
|
||||
|
||||
def __delete__(s, obj):
|
||||
warn()
|
||||
|
||||
def __get__(s, obj, owner):
|
||||
if obj is None:
|
||||
return self.descriptor
|
||||
warn()
|
||||
self.descriptor = NoninheritedConcreteProp()
|
||||
|
||||
|
||||
class SynonymProperty(DescriptorProperty):
|
||||
|
||||
def __init__(self, name, map_column=None,
|
||||
descriptor=None, comparator_factory=None,
|
||||
doc=None):
|
||||
self.name = name
|
||||
self.map_column = map_column
|
||||
self.descriptor = descriptor
|
||||
self.comparator_factory = comparator_factory
|
||||
self.doc = doc or (descriptor and descriptor.__doc__) or None
|
||||
|
||||
util.set_creation_order(self)
|
||||
|
||||
# TODO: when initialized, check _proxied_property,
|
||||
# emit a warning if its not a column-based property
|
||||
|
||||
@util.memoized_property
|
||||
def _proxied_property(self):
|
||||
return getattr(self.parent.class_, self.name).property
|
||||
|
||||
def _comparator_factory(self, mapper):
|
||||
prop = self._proxied_property
|
||||
|
||||
if self.comparator_factory:
|
||||
comp = self.comparator_factory(prop, mapper)
|
||||
else:
|
||||
comp = prop.comparator_factory(prop, mapper)
|
||||
return comp
|
||||
|
||||
def set_parent(self, parent, init):
|
||||
if self.map_column:
|
||||
# implement the 'map_column' option.
|
||||
if self.key not in parent.mapped_table.c:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Can't compile synonym '%s': no column on table "
|
||||
"'%s' named '%s'"
|
||||
% (self.name, parent.mapped_table.description, self.key))
|
||||
elif parent.mapped_table.c[self.key] in \
|
||||
parent._columntoproperty and \
|
||||
parent._columntoproperty[
|
||||
parent.mapped_table.c[self.key]
|
||||
].key == self.name:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Can't call map_column=True for synonym %r=%r, "
|
||||
"a ColumnProperty already exists keyed to the name "
|
||||
"%r for column %r" %
|
||||
(self.key, self.name, self.name, self.key)
|
||||
)
|
||||
p = properties.ColumnProperty(parent.mapped_table.c[self.key])
|
||||
parent._configure_property(
|
||||
self.name, p,
|
||||
init=init,
|
||||
setparent=True)
|
||||
p._mapped_by_synonym = self.key
|
||||
|
||||
self.parent = parent
|
||||
|
||||
|
||||
class ComparableProperty(DescriptorProperty):
|
||||
"""Instruments a Python property for use in query expressions."""
|
||||
|
||||
def __init__(self, comparator_factory, descriptor=None, doc=None):
|
||||
self.descriptor = descriptor
|
||||
self.comparator_factory = comparator_factory
|
||||
self.doc = doc or (descriptor and descriptor.__doc__) or None
|
||||
util.set_creation_order(self)
|
||||
|
||||
def _comparator_factory(self, mapper):
|
||||
return self.comparator_factory(self, mapper)
|
||||
Vendored
+362
@@ -0,0 +1,362 @@
|
||||
# orm/dynamic.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
|
||||
|
||||
"""Dynamic collection API.
|
||||
|
||||
Dynamic collections act like Query() objects for read operations and support
|
||||
basic add/delete mutation.
|
||||
|
||||
"""
|
||||
|
||||
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
|
||||
)
|
||||
from .query import Query
|
||||
|
||||
|
||||
class DynaLoader(strategies.AbstractRelationshipLoader):
|
||||
def init_class_attribute(self, mapper):
|
||||
self.is_class_level = True
|
||||
if not self.uselist:
|
||||
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,
|
||||
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,
|
||||
target_mapper, order_by, query_class=None, **kw):
|
||||
super(DynamicAttributeImpl, self).\
|
||||
__init__(class_, key, typecallable, dispatch, **kw)
|
||||
self.target_mapper = target_mapper
|
||||
self.order_by = order_by
|
||||
if not query_class:
|
||||
self.query_class = AppenderQuery
|
||||
elif AppenderMixin in query_class.mro():
|
||||
self.query_class = query_class
|
||||
else:
|
||||
self.query_class = mixin_user_query(query_class)
|
||||
|
||||
def get(self, state, dict_, passive=attributes.PASSIVE_OFF):
|
||||
if not passive & attributes.SQL_OK:
|
||||
return self._get_collection_history(state,
|
||||
attributes.PASSIVE_NO_INITIALIZE).added_items
|
||||
else:
|
||||
return self.query_class(self, state)
|
||||
|
||||
def get_collection(self, state, dict_, user_data=None,
|
||||
passive=attributes.PASSIVE_NO_INITIALIZE):
|
||||
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_plus_unchanged
|
||||
|
||||
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)
|
||||
|
||||
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=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)
|
||||
|
||||
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_,
|
||||
self,
|
||||
attributes.NEVER_SET)
|
||||
|
||||
# this is a hack to allow the fixtures.ComparableEntity fixture
|
||||
# to work
|
||||
dict_[self.key] = True
|
||||
return state.committed_state[self.key]
|
||||
|
||||
def set(self, state, dict_, value, initiator,
|
||||
passive=attributes.PASSIVE_OFF,
|
||||
check_old=None, pop=False):
|
||||
if initiator and initiator.parent_token is self.parent_token:
|
||||
return
|
||||
|
||||
if pop and value is None:
|
||||
return
|
||||
self._set_iterable(state, dict_, value)
|
||||
|
||||
def _set_iterable(self, state, dict_, iterable, adapter=None):
|
||||
new_values = list(iterable)
|
||||
if state.has_identity:
|
||||
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 = 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()
|
||||
|
||||
def set_committed_value(self, state, dict_, value):
|
||||
raise NotImplementedError("Dynamic attributes don't support "
|
||||
"collection population.")
|
||||
|
||||
def get_history(self, state, dict_, passive=attributes.PASSIVE_OFF):
|
||||
c = self._get_collection_history(state, passive)
|
||||
return c.as_history()
|
||||
|
||||
def get_all_pending(self, state, dict_):
|
||||
c = self._get_collection_history(
|
||||
state, attributes.PASSIVE_NO_INITIALIZE)
|
||||
return [
|
||||
(attributes.instance_state(x), x)
|
||||
for x in
|
||||
c.all_items
|
||||
]
|
||||
|
||||
def _get_collection_history(self, state, passive=attributes.PASSIVE_OFF):
|
||||
if self.key in state.committed_state:
|
||||
c = state.committed_state[self.key]
|
||||
else:
|
||||
c = CollectionHistory(self, state)
|
||||
|
||||
if state.has_identity and (passive & attributes.INIT_OK):
|
||||
return CollectionHistory(self, state, apply_to=c)
|
||||
else:
|
||||
return c
|
||||
|
||||
def append(self, state, dict_, value, initiator,
|
||||
passive=attributes.PASSIVE_OFF):
|
||||
if initiator is not self:
|
||||
self.fire_append_event(state, dict_, value, initiator)
|
||||
|
||||
def remove(self, state, dict_, value, initiator,
|
||||
passive=attributes.PASSIVE_OFF):
|
||||
if initiator is not self:
|
||||
self.fire_remove_event(state, dict_, value, initiator)
|
||||
|
||||
def pop(self, state, dict_, value, initiator,
|
||||
passive=attributes.PASSIVE_OFF):
|
||||
self.remove(state, dict_, value, initiator, passive=passive)
|
||||
|
||||
|
||||
class AppenderMixin(object):
|
||||
query_class = None
|
||||
|
||||
def __init__(self, attr, state):
|
||||
super(AppenderMixin, self).__init__(attr.target_mapper, None)
|
||||
self.instance = instance = state.obj()
|
||||
self.attr = attr
|
||||
|
||||
mapper = object_mapper(instance)
|
||||
prop = mapper._props[self.attr.key]
|
||||
self._criterion = prop.compare(
|
||||
operators.eq,
|
||||
instance,
|
||||
value_is_parent=True,
|
||||
alias_secondary=False)
|
||||
|
||||
if self.attr.order_by:
|
||||
self._order_by = self.attr.order_by
|
||||
|
||||
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 orm_util.has_identity(self.instance):
|
||||
return None
|
||||
else:
|
||||
return sess
|
||||
session = property(session, lambda s, x: None)
|
||||
|
||||
def __iter__(self):
|
||||
sess = self.session
|
||||
if sess is None:
|
||||
return iter(self.attr._get_collection_history(
|
||||
attributes.instance_state(self.instance),
|
||||
attributes.PASSIVE_NO_INITIALIZE).added_items)
|
||||
else:
|
||||
return iter(self._clone(sess))
|
||||
|
||||
def __getitem__(self, index):
|
||||
sess = self.session
|
||||
if sess is None:
|
||||
return self.attr._get_collection_history(
|
||||
attributes.instance_state(self.instance),
|
||||
attributes.PASSIVE_NO_INITIALIZE).indexed(index)
|
||||
else:
|
||||
return self._clone(sess).__getitem__(index)
|
||||
|
||||
def count(self):
|
||||
sess = self.session
|
||||
if sess is None:
|
||||
return len(self.attr._get_collection_history(
|
||||
attributes.instance_state(self.instance),
|
||||
attributes.PASSIVE_NO_INITIALIZE).added_items)
|
||||
else:
|
||||
return self._clone(sess).count()
|
||||
|
||||
def _clone(self, sess=None):
|
||||
# note we're returning an entirely new Query class instance
|
||||
# here without any assignment capabilities; the class of this
|
||||
# query is determined by the session.
|
||||
instance = self.instance
|
||||
if sess is None:
|
||||
sess = object_session(instance)
|
||||
if sess is None:
|
||||
raise orm_exc.DetachedInstanceError(
|
||||
"Parent instance %s is not bound to a Session, and no "
|
||||
"contextual session is established; lazy load operation "
|
||||
"of attribute '%s' cannot proceed" % (
|
||||
orm_util.instance_str(instance), self.attr.key))
|
||||
|
||||
if self.query_class:
|
||||
query = self.query_class(self.attr.target_mapper, session=sess)
|
||||
else:
|
||||
query = sess.query(self.attr.target_mapper)
|
||||
|
||||
query._criterion = self._criterion
|
||||
query._order_by = self._order_by
|
||||
|
||||
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),
|
||||
attributes.instance_dict(self.instance), item, None)
|
||||
|
||||
def remove(self, item):
|
||||
self.attr.remove(
|
||||
attributes.instance_state(self.instance),
|
||||
attributes.instance_dict(self.instance), item, None)
|
||||
|
||||
|
||||
class AppenderQuery(AppenderMixin, Query):
|
||||
"""A dynamic query that supports basic collection storage operations."""
|
||||
|
||||
|
||||
def mixin_user_query(cls):
|
||||
"""Return a new class with AppenderQuery functionality layered over."""
|
||||
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:
|
||||
coll = AppenderQuery(attr, state).autoflush(False)
|
||||
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 = 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)
|
||||
|
||||
Vendored
+125
@@ -0,0 +1,125 @@
|
||||
# orm/evaluator.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
|
||||
|
||||
import operator
|
||||
from ..sql import operators
|
||||
|
||||
|
||||
class UnevaluatableError(Exception):
|
||||
pass
|
||||
|
||||
_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'))
|
||||
|
||||
|
||||
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__)
|
||||
return meth(clause)
|
||||
|
||||
def visit_grouping(self, clause):
|
||||
return self.process(clause.element)
|
||||
|
||||
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'].\
|
||||
_columntoproperty[clause].key
|
||||
else:
|
||||
key = clause.key
|
||||
get_corresponding_attr = operator.attrgetter(key)
|
||||
return lambda obj: get_corresponding_attr(obj)
|
||||
|
||||
def visit_clauselist(self, clause):
|
||||
evaluators = map(self.process, clause.clauses)
|
||||
if clause.operator is operators.or_:
|
||||
def evaluate(obj):
|
||||
has_null = False
|
||||
for sub_evaluate in evaluators:
|
||||
value = sub_evaluate(obj)
|
||||
if value:
|
||||
return True
|
||||
has_null = has_null or value is None
|
||||
if has_null:
|
||||
return None
|
||||
return False
|
||||
elif clause.operator is operators.and_:
|
||||
def evaluate(obj):
|
||||
for sub_evaluate in evaluators:
|
||||
value = sub_evaluate(obj)
|
||||
if not value:
|
||||
if value is None:
|
||||
return None
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
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])
|
||||
operator = clause.operator
|
||||
if operator is operators.is_:
|
||||
def evaluate(obj):
|
||||
return eval_left(obj) == eval_right(obj)
|
||||
elif operator is operators.isnot:
|
||||
def evaluate(obj):
|
||||
return eval_left(obj) != eval_right(obj)
|
||||
elif operator in _straight_ops:
|
||||
def evaluate(obj):
|
||||
left_val = eval_left(obj)
|
||||
right_val = eval_right(obj)
|
||||
if left_val is None or right_val is None:
|
||||
return None
|
||||
return operator(eval_left(obj), eval_right(obj))
|
||||
else:
|
||||
raise UnevaluatableError(
|
||||
"Cannot evaluate %s with operator %s" %
|
||||
(type(clause).__name__, clause.operator))
|
||||
return evaluate
|
||||
|
||||
def visit_unary(self, clause):
|
||||
eval_inner = self.process(clause.element)
|
||||
if clause.operator is operators.inv:
|
||||
def evaluate(obj):
|
||||
value = eval_inner(obj)
|
||||
if value is None:
|
||||
return None
|
||||
return not value
|
||||
return evaluate
|
||||
raise UnevaluatableError(
|
||||
"Cannot evaluate %s with operator %s" %
|
||||
(type(clause).__name__, clause.operator))
|
||||
|
||||
def visit_bindparam(self, clause):
|
||||
val = clause.value
|
||||
return lambda obj: val
|
||||
Vendored
+1571
File diff suppressed because it is too large
Load Diff
Vendored
+163
@@ -0,0 +1,163 @@
|
||||
# orm/exc.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
|
||||
|
||||
"""SQLAlchemy ORM exceptions."""
|
||||
from .. import exc as sa_exc, util
|
||||
orm_util = util.importlater('sqlalchemy.orm', 'util')
|
||||
attributes = util.importlater('sqlalchemy.orm', 'attributes')
|
||||
|
||||
NO_STATE = (AttributeError, KeyError)
|
||||
"""Exception types that may be raised by instrumentation implementations."""
|
||||
|
||||
|
||||
class StaleDataError(sa_exc.SQLAlchemyError):
|
||||
"""An operation encountered database state that is unaccounted for.
|
||||
|
||||
Conditions which cause this to happen include:
|
||||
|
||||
* A flush may have attempted to update or delete rows
|
||||
and an unexpected number of rows were matched during
|
||||
the UPDATE or DELETE statement. Note that when
|
||||
version_id_col is used, rows in UPDATE or DELETE statements
|
||||
are also matched against the current known version
|
||||
identifier.
|
||||
|
||||
* A mapped object with version_id_col was refreshed,
|
||||
and the version number coming back from the database does
|
||||
not match that of the object itself.
|
||||
|
||||
* A object is detached from its parent object, however
|
||||
the object was previously attached to a different parent
|
||||
identity which was garbage collected, and a decision
|
||||
cannot be made if the new parent was really the most
|
||||
recent "parent".
|
||||
|
||||
.. versionadded:: 0.7.4
|
||||
|
||||
"""
|
||||
|
||||
ConcurrentModificationError = StaleDataError
|
||||
|
||||
|
||||
class FlushError(sa_exc.SQLAlchemyError):
|
||||
"""A invalid condition was detected during flush()."""
|
||||
|
||||
|
||||
class UnmappedError(sa_exc.InvalidRequestError):
|
||||
"""Base for exceptions that involve expected mappings not present."""
|
||||
|
||||
|
||||
class ObjectDereferencedError(sa_exc.SQLAlchemyError):
|
||||
"""An operation cannot complete due to an object being garbage
|
||||
collected.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class DetachedInstanceError(sa_exc.SQLAlchemyError):
|
||||
"""An attempt to access unloaded attributes on a
|
||||
mapped instance that is detached."""
|
||||
|
||||
|
||||
class UnmappedInstanceError(UnmappedError):
|
||||
"""An mapping operation was requested for an unknown instance."""
|
||||
|
||||
def __init__(self, obj, msg=None):
|
||||
if not msg:
|
||||
try:
|
||||
mapper = orm_util.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))
|
||||
except UnmappedClassError:
|
||||
msg = _default_unmapped(type(obj))
|
||||
if isinstance(obj, type):
|
||||
msg += (
|
||||
'; was a class (%s) supplied where an instance was '
|
||||
'required?' % _safe_cls_name(obj))
|
||||
UnmappedError.__init__(self, msg)
|
||||
|
||||
def __reduce__(self):
|
||||
return self.__class__, (None, self.args[0])
|
||||
|
||||
|
||||
class UnmappedClassError(UnmappedError):
|
||||
"""An mapping operation was requested for an unknown class."""
|
||||
|
||||
def __init__(self, cls, msg=None):
|
||||
if not msg:
|
||||
msg = _default_unmapped(cls)
|
||||
UnmappedError.__init__(self, msg)
|
||||
|
||||
def __reduce__(self):
|
||||
return self.__class__, (None, self.args[0])
|
||||
|
||||
|
||||
class ObjectDeletedError(sa_exc.InvalidRequestError):
|
||||
"""A refresh operation failed to retrieve the database
|
||||
row corresponding to an object's known primary key identity.
|
||||
|
||||
A refresh operation proceeds when an expired attribute is
|
||||
accessed on an object, or when :meth:`.Query.get` is
|
||||
used to retrieve an object which is, upon retrieval, detected
|
||||
as expired. A SELECT is emitted for the target row
|
||||
based on primary key; if no row is returned, this
|
||||
exception is raised.
|
||||
|
||||
The true meaning of this exception is simply that
|
||||
no row exists for the primary key identifier associated
|
||||
with a persistent object. The row may have been
|
||||
deleted, or in some cases the primary key updated
|
||||
to a new value, outside of the ORM's management of the target
|
||||
object.
|
||||
|
||||
"""
|
||||
def __init__(self, state, msg=None):
|
||||
if not msg:
|
||||
msg = "Instance '%s' has been deleted, or its "\
|
||||
"row is otherwise not present." % orm_util.state_str(state)
|
||||
|
||||
sa_exc.InvalidRequestError.__init__(self, msg)
|
||||
|
||||
def __reduce__(self):
|
||||
return self.__class__, (None, self.args[0])
|
||||
|
||||
|
||||
class UnmappedColumnError(sa_exc.InvalidRequestError):
|
||||
"""Mapping operation was requested on an unknown column."""
|
||||
|
||||
|
||||
class NoResultFound(sa_exc.InvalidRequestError):
|
||||
"""A database result was required but none was found."""
|
||||
|
||||
|
||||
class MultipleResultsFound(sa_exc.InvalidRequestError):
|
||||
"""A single database result was required but more than one were found."""
|
||||
|
||||
|
||||
def _safe_cls_name(cls):
|
||||
try:
|
||||
cls_name = '.'.join((cls.__module__, cls.__name__))
|
||||
except AttributeError:
|
||||
cls_name = getattr(cls, '__name__', None)
|
||||
if cls_name is None:
|
||||
cls_name = repr(cls)
|
||||
return cls_name
|
||||
|
||||
|
||||
def _default_unmapped(cls):
|
||||
try:
|
||||
mappers = attributes.manager_of_class(cls).mappers
|
||||
except NO_STATE:
|
||||
mappers = {}
|
||||
except TypeError:
|
||||
mappers = {}
|
||||
name = _safe_cls_name(cls)
|
||||
|
||||
if not mappers:
|
||||
return "Class '%s' is not mapped" % name
|
||||
Vendored
+243
@@ -0,0 +1,243 @@
|
||||
# orm/identity.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
|
||||
|
||||
import weakref
|
||||
from . import attributes
|
||||
|
||||
|
||||
class IdentityMap(dict):
|
||||
def __init__(self):
|
||||
self._modified = set()
|
||||
self._wr = weakref.ref(self)
|
||||
|
||||
def replace(self, state):
|
||||
raise NotImplementedError()
|
||||
|
||||
def add(self, state):
|
||||
raise NotImplementedError()
|
||||
|
||||
def update(self, dict):
|
||||
raise NotImplementedError("IdentityMap uses add() to insert data")
|
||||
|
||||
def clear(self):
|
||||
raise NotImplementedError("IdentityMap uses remove() to remove data")
|
||||
|
||||
def _manage_incoming_state(self, state):
|
||||
state._instance_dict = self._wr
|
||||
|
||||
if state.modified:
|
||||
self._modified.add(state)
|
||||
|
||||
def _manage_removed_state(self, state):
|
||||
del state._instance_dict
|
||||
self._modified.discard(state)
|
||||
|
||||
def _dirty_states(self):
|
||||
return self._modified
|
||||
|
||||
def check_modified(self):
|
||||
"""return True if any InstanceStates present have been marked
|
||||
as 'modified'.
|
||||
|
||||
"""
|
||||
return bool(self._modified)
|
||||
|
||||
def has_key(self, key):
|
||||
return key in self
|
||||
|
||||
def popitem(self):
|
||||
raise NotImplementedError("IdentityMap uses remove() to remove data")
|
||||
|
||||
def pop(self, key, *args):
|
||||
raise NotImplementedError("IdentityMap uses remove() to remove data")
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
raise NotImplementedError("IdentityMap uses add() to insert data")
|
||||
|
||||
def copy(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
raise NotImplementedError("IdentityMap uses add() to insert data")
|
||||
|
||||
def __delitem__(self, key):
|
||||
raise NotImplementedError("IdentityMap uses remove() to remove data")
|
||||
|
||||
|
||||
class WeakInstanceDict(IdentityMap):
|
||||
def __init__(self):
|
||||
IdentityMap.__init__(self)
|
||||
|
||||
def __getitem__(self, key):
|
||||
state = dict.__getitem__(self, key)
|
||||
o = state.obj()
|
||||
if o is None:
|
||||
raise KeyError, key
|
||||
return o
|
||||
|
||||
def __contains__(self, key):
|
||||
try:
|
||||
if dict.__contains__(self, key):
|
||||
state = dict.__getitem__(self, key)
|
||||
o = state.obj()
|
||||
else:
|
||||
return False
|
||||
except KeyError:
|
||||
return False
|
||||
else:
|
||||
return o is not None
|
||||
|
||||
def contains_state(self, state):
|
||||
return dict.get(self, state.key) is state
|
||||
|
||||
def replace(self, state):
|
||||
if dict.__contains__(self, state.key):
|
||||
existing = dict.__getitem__(self, state.key)
|
||||
if existing is not state:
|
||||
self._manage_removed_state(existing)
|
||||
else:
|
||||
return
|
||||
|
||||
dict.__setitem__(self, state.key, state)
|
||||
self._manage_incoming_state(state)
|
||||
|
||||
def add(self, state):
|
||||
key = state.key
|
||||
# inline of self.__contains__
|
||||
if dict.__contains__(self, key):
|
||||
try:
|
||||
existing_state = dict.__getitem__(self, key)
|
||||
if existing_state is not state:
|
||||
o = existing_state.obj()
|
||||
if o is not None:
|
||||
raise AssertionError(
|
||||
"A conflicting state is already "
|
||||
"present in the identity map for key %r"
|
||||
% (key, ))
|
||||
else:
|
||||
return
|
||||
except KeyError:
|
||||
pass
|
||||
dict.__setitem__(self, key, state)
|
||||
self._manage_incoming_state(state)
|
||||
|
||||
def get(self, key, default=None):
|
||||
state = dict.get(self, key, default)
|
||||
if state is default:
|
||||
return default
|
||||
o = state.obj()
|
||||
if o is None:
|
||||
return default
|
||||
return o
|
||||
|
||||
def _items(self):
|
||||
values = self.all_states()
|
||||
result = []
|
||||
for state in values:
|
||||
value = state.obj()
|
||||
if value is not None:
|
||||
result.append((state.key, value))
|
||||
return result
|
||||
|
||||
def _values(self):
|
||||
values = self.all_states()
|
||||
result = []
|
||||
for state in values:
|
||||
value = state.obj()
|
||||
if value is not None:
|
||||
result.append(value)
|
||||
|
||||
return result
|
||||
|
||||
# Py3K
|
||||
#def items(self):
|
||||
# return iter(self._items())
|
||||
#
|
||||
#def values(self):
|
||||
# return iter(self._values())
|
||||
# Py2K
|
||||
items = _items
|
||||
|
||||
def iteritems(self):
|
||||
return iter(self.items())
|
||||
|
||||
values = _values
|
||||
|
||||
def itervalues(self):
|
||||
return iter(self.values())
|
||||
# end Py2K
|
||||
|
||||
def all_states(self):
|
||||
# Py3K
|
||||
# return list(dict.values(self))
|
||||
# Py2K
|
||||
return dict.values(self)
|
||||
# end Py2K
|
||||
|
||||
def discard(self, state):
|
||||
st = dict.get(self, state.key, None)
|
||||
if st is state:
|
||||
dict.pop(self, state.key, None)
|
||||
self._manage_removed_state(state)
|
||||
|
||||
def prune(self):
|
||||
return 0
|
||||
|
||||
|
||||
class StrongInstanceDict(IdentityMap):
|
||||
def all_states(self):
|
||||
return [attributes.instance_state(o) for o in self.itervalues()]
|
||||
|
||||
def contains_state(self, 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):
|
||||
existing = dict.__getitem__(self, state.key)
|
||||
existing = attributes.instance_state(existing)
|
||||
if existing is not state:
|
||||
self._manage_removed_state(existing)
|
||||
else:
|
||||
return
|
||||
|
||||
dict.__setitem__(self, state.key, state.obj())
|
||||
self._manage_incoming_state(state)
|
||||
|
||||
def add(self, state):
|
||||
if state.key in self:
|
||||
if attributes.instance_state(dict.__getitem__(self,
|
||||
state.key)) is not state:
|
||||
raise AssertionError('A conflicting state is already '
|
||||
'present in the identity map for key %r'
|
||||
% (state.key, ))
|
||||
else:
|
||||
dict.__setitem__(self, state.key, state.obj())
|
||||
self._manage_incoming_state(state)
|
||||
|
||||
def discard(self, state):
|
||||
obj = dict.get(self, state.key, None)
|
||||
if obj is not None:
|
||||
st = attributes.instance_state(obj)
|
||||
if st is state:
|
||||
dict.pop(self, state.key, None)
|
||||
self._manage_removed_state(state)
|
||||
|
||||
def prune(self):
|
||||
"""prune unreferenced, non-dirty states."""
|
||||
|
||||
ref_count = len(self)
|
||||
dirty = [s.obj() for s in self.all_states() if s.modified]
|
||||
|
||||
# work around http://bugs.python.org/issue6149
|
||||
keepers = weakref.WeakValueDictionary()
|
||||
keepers.update(self)
|
||||
|
||||
dict.clear(self)
|
||||
dict.update(self, keepers)
|
||||
self.modified = bool(dirty)
|
||||
return ref_count - len(self)
|
||||
+464
@@ -0,0 +1,464 @@
|
||||
# orm/instrumentation.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
|
||||
|
||||
"""Defines SQLAlchemy's system of class instrumentation.
|
||||
|
||||
This module is usually not directly visible to user applications, but
|
||||
defines a large part of the ORM's interactivity.
|
||||
|
||||
instrumentation.py deals with registration of end-user classes
|
||||
for state tracking. It interacts closely with state.py
|
||||
and attributes.py which establish per-instance and per-class-attribute
|
||||
instrumentation, respectively.
|
||||
|
||||
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 . import exc, collections, events, interfaces
|
||||
from operator import attrgetter
|
||||
from .. import event, util
|
||||
state = util.importlater("sqlalchemy.orm", "state")
|
||||
|
||||
|
||||
class ClassManager(dict):
|
||||
"""tracks state information at the class level."""
|
||||
|
||||
MANAGER_ATTR = '_sa_class_manager'
|
||||
STATE_ATTR = '_sa_instance_state'
|
||||
|
||||
deferred_scalar_loader = None
|
||||
|
||||
original_init = object.__init__
|
||||
|
||||
factory = None
|
||||
|
||||
def __init__(self, class_):
|
||||
self.class_ = class_
|
||||
self.info = {}
|
||||
self.new_init = None
|
||||
self.local_attrs = {}
|
||||
self.originals = {}
|
||||
|
||||
self._bases = [mgr for mgr in [
|
||||
manager_of_class(base)
|
||||
for base in self.class_.__bases__
|
||||
if isinstance(base, type)
|
||||
] if mgr is not None]
|
||||
|
||||
for base in self._bases:
|
||||
self.update(base)
|
||||
|
||||
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()
|
||||
|
||||
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_)
|
||||
|
||||
dispatch = event.dispatcher(events.InstanceEvents)
|
||||
|
||||
@property
|
||||
def is_mapped(self):
|
||||
return 'mapper' in self.__dict__
|
||||
|
||||
@util.memoized_property
|
||||
def mapper(self):
|
||||
# raises unless self.mapper has been assigned
|
||||
raise exc.UnmappedClassError(self.class_)
|
||||
|
||||
def _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.
|
||||
|
||||
i.e. has an impl.
|
||||
"""
|
||||
|
||||
return key in self and self[key].impl is not None
|
||||
|
||||
def _subclass_manager(self, cls):
|
||||
"""Create a new ClassManager for a subclass of this ClassManager's
|
||||
class.
|
||||
|
||||
This is called automatically when attributes are instrumented so that
|
||||
the attributes can be propagated to subclasses against their own
|
||||
class-local manager, without the need for mappers etc. to have already
|
||||
pre-configured managers for the full class hierarchy. Mappers
|
||||
can post-configure the auto-generated ClassManager when needed.
|
||||
|
||||
"""
|
||||
manager = manager_of_class(cls)
|
||||
if manager is None:
|
||||
manager = _instrumentation_factory.create_manager_for_cls(cls)
|
||||
return manager
|
||||
|
||||
def _instrument_init(self):
|
||||
# TODO: self.class_.__init__ is often the already-instrumented
|
||||
# __init__ from an instrumented superclass. We still need to make
|
||||
# our own wrapper, but it would
|
||||
# be nice to wrap the original __init__ and not our existing wrapper
|
||||
# of such, since this adds method overhead.
|
||||
self.original_init = self.class_.__init__
|
||||
self.new_init = _generate_init(self.class_, self)
|
||||
self.install_member('__init__', self.new_init)
|
||||
|
||||
def _uninstrument_init(self):
|
||||
if self.new_init:
|
||||
self.uninstall_member('__init__')
|
||||
self.new_init = None
|
||||
|
||||
@util.memoized_property
|
||||
def _state_constructor(self):
|
||||
self.dispatch.first_init(self, self.class_)
|
||||
return state.InstanceState
|
||||
|
||||
def manage(self):
|
||||
"""Mark this instance as the manager for its class."""
|
||||
|
||||
setattr(self.class_, self.MANAGER_ATTR, self)
|
||||
|
||||
def dispose(self):
|
||||
"""Dissasociate this manager from its class."""
|
||||
|
||||
delattr(self.class_, self.MANAGER_ATTR)
|
||||
|
||||
@util.hybridmethod
|
||||
def manager_getter(self):
|
||||
def manager_of_class(cls):
|
||||
return cls.__dict__.get(ClassManager.MANAGER_ATTR, None)
|
||||
return manager_of_class
|
||||
|
||||
@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 attrgetter(self.STATE_ATTR)
|
||||
|
||||
@util.hybridmethod
|
||||
def dict_getter(self):
|
||||
return attrgetter('__dict__')
|
||||
|
||||
def instrument_attribute(self, key, inst, propagated=False):
|
||||
if propagated:
|
||||
if key in self.local_attrs:
|
||||
return # don't override local attr with inherited attr
|
||||
else:
|
||||
self.local_attrs[key] = inst
|
||||
self.install_descriptor(key, inst)
|
||||
self[key] = inst
|
||||
|
||||
for cls in self.class_.__subclasses__():
|
||||
manager = self._subclass_manager(cls)
|
||||
manager.instrument_attribute(key, inst, True)
|
||||
|
||||
def subclass_managers(self, recursive):
|
||||
for cls in self.class_.__subclasses__():
|
||||
mgr = manager_of_class(cls)
|
||||
if mgr is not None and mgr is not self:
|
||||
yield mgr
|
||||
if recursive:
|
||||
for m in mgr.subclass_managers(True):
|
||||
yield m
|
||||
|
||||
def post_configure_attribute(self, key):
|
||||
_instrumentation_factory.dispatch.\
|
||||
attribute_instrument(self.class_, key, self[key])
|
||||
|
||||
def uninstrument_attribute(self, key, propagated=False):
|
||||
if key not in self:
|
||||
return
|
||||
if propagated:
|
||||
if key in self.local_attrs:
|
||||
return # don't get rid of local attr
|
||||
else:
|
||||
del self.local_attrs[key]
|
||||
self.uninstall_descriptor(key)
|
||||
del self[key]
|
||||
for cls in self.class_.__subclasses__():
|
||||
manager = manager_of_class(cls)
|
||||
if manager:
|
||||
manager.uninstrument_attribute(key, True)
|
||||
|
||||
def unregister(self):
|
||||
"""remove all instrumentation established by this ClassManager."""
|
||||
|
||||
self._uninstrument_init()
|
||||
|
||||
self.mapper = self.dispatch = None
|
||||
self.info.clear()
|
||||
|
||||
for key in list(self):
|
||||
if key in self.local_attrs:
|
||||
self.uninstrument_attribute(key)
|
||||
|
||||
def install_descriptor(self, key, inst):
|
||||
if key in (self.STATE_ATTR, self.MANAGER_ATTR):
|
||||
raise KeyError("%r: requested attribute name conflicts with "
|
||||
"instrumentation attribute of the same name." %
|
||||
key)
|
||||
setattr(self.class_, key, inst)
|
||||
|
||||
def uninstall_descriptor(self, key):
|
||||
delattr(self.class_, key)
|
||||
|
||||
def install_member(self, key, implementation):
|
||||
if key in (self.STATE_ATTR, self.MANAGER_ATTR):
|
||||
raise KeyError("%r: requested attribute name conflicts with "
|
||||
"instrumentation attribute of the same name." %
|
||||
key)
|
||||
self.originals.setdefault(key, getattr(self.class_, key, None))
|
||||
setattr(self.class_, key, implementation)
|
||||
|
||||
def uninstall_member(self, key):
|
||||
original = self.originals.pop(key, None)
|
||||
if original is not None:
|
||||
setattr(self.class_, key, original)
|
||||
|
||||
def instrument_collection_class(self, key, collection_class):
|
||||
return collections.prepare_instrumentation(collection_class)
|
||||
|
||||
def initialize_collection(self, key, state, factory):
|
||||
user_data = factory()
|
||||
adapter = collections.CollectionAdapter(
|
||||
self.get_impl(key), state, user_data)
|
||||
return adapter, user_data
|
||||
|
||||
def is_instrumented(self, key, search=False):
|
||||
if search:
|
||||
return key in self
|
||||
else:
|
||||
return key in self.local_attrs
|
||||
|
||||
def get_impl(self, key):
|
||||
return self[key].impl
|
||||
|
||||
@property
|
||||
def attributes(self):
|
||||
return self.itervalues()
|
||||
|
||||
## InstanceState management
|
||||
|
||||
def new_instance(self, state=None):
|
||||
instance = self.class_.__new__(self.class_)
|
||||
setattr(instance, self.STATE_ATTR,
|
||||
state or self._state_constructor(instance, self))
|
||||
return instance
|
||||
|
||||
def setup_instance(self, instance, state=None):
|
||||
setattr(instance, self.STATE_ATTR,
|
||||
state or self._state_constructor(instance, self))
|
||||
|
||||
def teardown_instance(self, instance):
|
||||
delattr(instance, self.STATE_ATTR)
|
||||
|
||||
def _new_state_if_none(self, instance):
|
||||
"""Install a default InstanceState if none is present.
|
||||
|
||||
A private convenience method used by the __init__ decorator.
|
||||
|
||||
"""
|
||||
if hasattr(instance, self.STATE_ATTR):
|
||||
return False
|
||||
elif self.class_ is not instance.__class__ and \
|
||||
self.is_mapped:
|
||||
# this will create a new ClassManager for the
|
||||
# subclass, without a mapper. This is likely a
|
||||
# user error situation but allow the object
|
||||
# to be constructed, so that it is usable
|
||||
# in a non-ORM context at least.
|
||||
return self._subclass_manager(instance.__class__).\
|
||||
_new_state_if_none(instance)
|
||||
else:
|
||||
state = self._state_constructor(instance, self)
|
||||
setattr(instance, self.STATE_ATTR, state)
|
||||
return state
|
||||
|
||||
def has_state(self, instance):
|
||||
return hasattr(instance, self.STATE_ATTR)
|
||||
|
||||
def has_parent(self, state, key, optimistic=False):
|
||||
"""TODO"""
|
||||
return self.get_impl(key).hasparent(state, optimistic=optimistic)
|
||||
|
||||
def __nonzero__(self):
|
||||
"""All ClassManagers are non-zero regardless of attribute state."""
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s of %r at %x>' % (
|
||||
self.__class__.__name__, self.class_, id(self))
|
||||
|
||||
|
||||
class InstrumentationFactory(object):
|
||||
"""Factory for new ClassManager instances."""
|
||||
|
||||
dispatch = event.dispatcher(events.InstrumentationEvents)
|
||||
|
||||
def create_manager_for_cls(self, class_):
|
||||
assert class_ is not None
|
||||
assert manager_of_class(class_) is None
|
||||
|
||||
# give a more complicated subclass
|
||||
# a chance to do what it wants here
|
||||
manager, factory = self._locate_extended_factory(class_)
|
||||
|
||||
if factory is None:
|
||||
factory = ClassManager
|
||||
manager = factory(class_)
|
||||
|
||||
self._check_conflicts(class_, factory)
|
||||
|
||||
manager.factory = factory
|
||||
|
||||
self.dispatch.class_instrument(class_)
|
||||
return manager
|
||||
|
||||
def _locate_extended_factory(self, class_):
|
||||
"""Overridden by a subclass to do an extended lookup."""
|
||||
return None, None
|
||||
|
||||
def _check_conflicts(self, class_, factory):
|
||||
"""Overridden by a subclass to test for conflicting factories."""
|
||||
return
|
||||
|
||||
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)
|
||||
|
||||
# this attribute is replaced by sqlalchemy.ext.instrumentation
|
||||
# when importred.
|
||||
_instrumentation_factory = InstrumentationFactory()
|
||||
|
||||
|
||||
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 = _instrumentation_factory.create_manager_for_cls(class_)
|
||||
return manager
|
||||
|
||||
|
||||
def unregister_class(class_):
|
||||
"""Unregister class instrumentation."""
|
||||
|
||||
_instrumentation_factory.unregister(class_)
|
||||
|
||||
|
||||
def is_instrumented(instance, key):
|
||||
"""Return True if the given attribute on the given instance is
|
||||
instrumented by the attributes package.
|
||||
|
||||
This function may be used regardless of instrumentation
|
||||
applied directly to the class, i.e. no descriptors are required.
|
||||
|
||||
"""
|
||||
return manager_of_class(instance.__class__).\
|
||||
is_instrumented(key, search=True)
|
||||
|
||||
# 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 = ClassManager.state_getter()
|
||||
|
||||
instance_dict = _default_dict_getter = ClassManager.dict_getter()
|
||||
|
||||
manager_of_class = _default_manager_getter = ClassManager.manager_getter()
|
||||
|
||||
|
||||
def _generate_init(class_, class_manager):
|
||||
"""Build an __init__ decorator that triggers ClassManager events."""
|
||||
|
||||
# TODO: we should use the ClassManager's notion of the
|
||||
# original '__init__' method, once ClassManager is fixed
|
||||
# to always reference that.
|
||||
original__init__ = class_.__init__
|
||||
assert original__init__
|
||||
|
||||
# Go through some effort here and don't change the user's __init__
|
||||
# calling signature, including the unlikely case that it has
|
||||
# a return value.
|
||||
# FIXME: need to juggle local names to avoid constructor argument
|
||||
# clashes.
|
||||
func_body = """\
|
||||
def __init__(%(apply_pos)s):
|
||||
new_state = class_manager._new_state_if_none(%(self_arg)s)
|
||||
if new_state:
|
||||
return new_state._initialize_instance(%(apply_kw)s)
|
||||
else:
|
||||
return original__init__(%(apply_kw)s)
|
||||
"""
|
||||
func_vars = util.format_argspec_init(original__init__, grouped=False)
|
||||
func_text = func_body % func_vars
|
||||
|
||||
# Py3K
|
||||
#func_defaults = getattr(original__init__, '__defaults__', None)
|
||||
#func_kw_defaults = getattr(original__init__, '__kwdefaults__', None)
|
||||
# Py2K
|
||||
func = getattr(original__init__, 'im_func', original__init__)
|
||||
func_defaults = getattr(func, 'func_defaults', None)
|
||||
# end Py2K
|
||||
|
||||
env = locals().copy()
|
||||
exec func_text in env
|
||||
__init__ = env['__init__']
|
||||
__init__.__doc__ = original__init__.__doc__
|
||||
if func_defaults:
|
||||
__init__.func_defaults = func_defaults
|
||||
# Py3K
|
||||
#if func_kw_defaults:
|
||||
# __init__.__kwdefaults__ = func_kw_defaults
|
||||
return __init__
|
||||
Vendored
+858
@@ -0,0 +1,858 @@
|
||||
# orm/interfaces.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
|
||||
|
||||
"""
|
||||
|
||||
Contains various base classes used throughout the ORM.
|
||||
|
||||
Defines the now deprecated ORM extension classes as well
|
||||
as ORM internals.
|
||||
|
||||
Other than the deprecated extensions, this module and the
|
||||
classes within should be considered mostly private.
|
||||
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from .. import exc as sa_exc, util, inspect
|
||||
from ..sql import operators
|
||||
from collections import deque
|
||||
|
||||
orm_util = util.importlater('sqlalchemy.orm', 'util')
|
||||
collections = util.importlater('sqlalchemy.orm', 'collections')
|
||||
|
||||
__all__ = (
|
||||
'AttributeExtension',
|
||||
'EXT_CONTINUE',
|
||||
'EXT_STOP',
|
||||
'ExtensionOption',
|
||||
'InstrumentationManager',
|
||||
'LoaderStrategy',
|
||||
'MapperExtension',
|
||||
'MapperOption',
|
||||
'MapperProperty',
|
||||
'PropComparator',
|
||||
'PropertyOption',
|
||||
'SessionExtension',
|
||||
'StrategizedOption',
|
||||
'StrategizedProperty',
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
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,
|
||||
attribute access, loading behavior, and dependency calculations.
|
||||
|
||||
The most common occurrences of :class:`.MapperProperty` are the
|
||||
mapped :class:`.Column`, which is represented in a mapping as
|
||||
an instance of :class:`.ColumnProperty`,
|
||||
and a reference to another class produced by :func:`.relationship`,
|
||||
represented in the mapping as an instance of
|
||||
:class:`.RelationshipProperty`.
|
||||
|
||||
"""
|
||||
|
||||
cascade = frozenset()
|
||||
"""The set of 'cascade' attribute names.
|
||||
|
||||
This collection is checked before the 'cascade_iterator' method is called.
|
||||
|
||||
"""
|
||||
|
||||
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
|
||||
statement referenced by the query context, adding columns and/or
|
||||
criterion as appropriate.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def create_row_processor(self, context, path,
|
||||
mapper, row, adapter):
|
||||
"""Return a 3-tuple consisting of three row processing functions.
|
||||
|
||||
"""
|
||||
return None, None, None
|
||||
|
||||
def cascade_iterator(self, type_, state, visited_instances=None,
|
||||
halt_on=None):
|
||||
"""Iterate through instances related to the given instance for
|
||||
a particular 'cascade', starting with this MapperProperty.
|
||||
|
||||
Return an iterator3-tuples (instance, mapper, state).
|
||||
|
||||
Note that the 'cascade' collection on this MapperProperty is
|
||||
checked first for the given type before cascade_iterator is called.
|
||||
|
||||
See PropertyLoader for the related instance implementation.
|
||||
"""
|
||||
|
||||
return iter(())
|
||||
|
||||
def set_parent(self, parent, init):
|
||||
self.parent = parent
|
||||
|
||||
def instrument_class(self, mapper): # pragma: no-coverage
|
||||
raise NotImplementedError()
|
||||
|
||||
@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
|
||||
relationships between mappers and perform other post-mapper-creation
|
||||
initialization steps.
|
||||
|
||||
"""
|
||||
self._configure_started = True
|
||||
self.do_init()
|
||||
self._configure_finished = True
|
||||
|
||||
@property
|
||||
def class_attribute(self):
|
||||
"""Return the class-bound descriptor corresponding to this
|
||||
: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)
|
||||
|
||||
def do_init(self):
|
||||
"""Perform subclass-specific initialization post-mapper-creation
|
||||
steps.
|
||||
|
||||
This is a template method called by the ``MapperProperty``
|
||||
object's init() method.
|
||||
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def post_instrument_class(self, mapper):
|
||||
"""Perform instrumentation adjustments that need to occur
|
||||
after init() has completed.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def is_primary(self):
|
||||
"""Return True if this ``MapperProperty``'s mapper is the
|
||||
primary mapper for its class.
|
||||
|
||||
This flag is used to indicate that the ``MapperProperty`` can
|
||||
define attribute instrumentation for the class at the class
|
||||
level (as opposed to the individual instance level).
|
||||
"""
|
||||
|
||||
return not self.parent.non_primary
|
||||
|
||||
def merge(self, session, source_state, source_dict, dest_state,
|
||||
dest_dict, load, _recursive):
|
||||
"""Merge the attribute represented by this ``MapperProperty``
|
||||
from source to destination object"""
|
||||
|
||||
pass
|
||||
|
||||
def compare(self, operator, value, **kw):
|
||||
"""Return a compare operation for the columns represented by
|
||||
this ``MapperProperty`` to the given value, which may be a
|
||||
column value or an instance. 'operator' is an operator from
|
||||
the operators module, or from sql.Comparator.
|
||||
|
||||
By default uses the PropComparator attached to this MapperProperty
|
||||
under the attribute name "comparator".
|
||||
"""
|
||||
|
||||
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 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
|
||||
: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 :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):
|
||||
def __eq__(self, other):
|
||||
return self.__clause_element__() == other
|
||||
|
||||
class MyRelationshipComparator(RelationshipProperty.Comparator):
|
||||
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, parentmapper, adapter=None):
|
||||
self.prop = self.property = prop
|
||||
self._parentmapper = parentmapper
|
||||
self.adapter = adapter
|
||||
|
||||
def __clause_element__(self):
|
||||
raise NotImplementedError("%r" % self)
|
||||
|
||||
def adapted(self, adapter):
|
||||
"""Return a copy of this PropComparator which will use the given
|
||||
adaption function on the local side of generated expressions.
|
||||
|
||||
"""
|
||||
|
||||
return self.__class__(self.prop, self._parentmapper, adapter)
|
||||
|
||||
@util.memoized_property
|
||||
def info(self):
|
||||
return self.property.info
|
||||
|
||||
@staticmethod
|
||||
def any_op(a, b, **kwargs):
|
||||
return a.any(b, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def has_op(a, b, **kwargs):
|
||||
return a.has(b, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def of_type_op(a, class_):
|
||||
return a.of_type(class_)
|
||||
|
||||
def of_type(self, class_):
|
||||
"""Redefine this object in terms of a polymorphic subclass.
|
||||
|
||||
Returns a new PropComparator from which further criterion can be
|
||||
evaluated.
|
||||
|
||||
e.g.::
|
||||
|
||||
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.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
return self.operate(PropComparator.of_type_op, class_)
|
||||
|
||||
def any(self, criterion=None, **kwargs):
|
||||
"""Return true if this collection contains any member that meets the
|
||||
given criterion.
|
||||
|
||||
The usual implementation of ``any()`` is
|
||||
:meth:`.RelationshipProperty.Comparator.any`.
|
||||
|
||||
:param criterion: an optional ClauseElement formulated against the
|
||||
member class' table or attributes.
|
||||
|
||||
:param \**kwargs: key/value pairs corresponding to member class
|
||||
attribute names which will be compared via equality to the
|
||||
corresponding values.
|
||||
|
||||
"""
|
||||
|
||||
return self.operate(PropComparator.any_op, criterion, **kwargs)
|
||||
|
||||
def has(self, criterion=None, **kwargs):
|
||||
"""Return true if this element references a member which meets the
|
||||
given criterion.
|
||||
|
||||
The usual implementation of ``has()`` is
|
||||
:meth:`.RelationshipProperty.Comparator.has`.
|
||||
|
||||
:param criterion: an optional ClauseElement formulated against the
|
||||
member class' table or attributes.
|
||||
|
||||
:param \**kwargs: key/value pairs corresponding to member class
|
||||
attribute names which will be compared via equality to the
|
||||
corresponding values.
|
||||
|
||||
"""
|
||||
|
||||
return self.operate(PropComparator.has_op, criterion, **kwargs)
|
||||
|
||||
|
||||
class StrategizedProperty(MapperProperty):
|
||||
"""A MapperProperty which uses selectable strategies to affect
|
||||
loading behavior.
|
||||
|
||||
There is a single strategy selected by default. Alternate
|
||||
strategies can be selected at Query time through the usage of
|
||||
``StrategizedOption`` objects via the Query.options() method.
|
||||
|
||||
"""
|
||||
|
||||
strategy_wildcard_key = None
|
||||
|
||||
@util.memoized_property
|
||||
def _wildcard_path(self):
|
||||
if self.strategy_wildcard_key:
|
||||
return ('loaderstrategy', (self.strategy_wildcard_key,))
|
||||
else:
|
||||
return None
|
||||
|
||||
def _get_context_strategy(self, context, path):
|
||||
strategy_cls = path._inlined_get_for(self, context, 'loaderstrategy')
|
||||
|
||||
if not strategy_cls:
|
||||
wc_key = self._wildcard_path
|
||||
if wc_key and wc_key in context.attributes:
|
||||
strategy_cls = context.attributes[wc_key]
|
||||
|
||||
if strategy_cls:
|
||||
try:
|
||||
return self._strategies[strategy_cls]
|
||||
except KeyError:
|
||||
return self.__init_strategy(strategy_cls)
|
||||
return self.strategy
|
||||
|
||||
def _get_strategy(self, cls):
|
||||
try:
|
||||
return self._strategies[cls]
|
||||
except KeyError:
|
||||
return self.__init_strategy(cls)
|
||||
|
||||
def __init_strategy(self, cls):
|
||||
self._strategies[cls] = strategy = cls(self)
|
||||
return strategy
|
||||
|
||||
def setup(self, context, entity, path, adapter, **kwargs):
|
||||
self._get_context_strategy(context, path).\
|
||||
setup_query(context, entity, path,
|
||||
adapter, **kwargs)
|
||||
|
||||
def create_row_processor(self, context, path, mapper, row, adapter):
|
||||
return self._get_context_strategy(context, path).\
|
||||
create_row_processor(context, path,
|
||||
mapper, row, adapter)
|
||||
|
||||
def do_init(self):
|
||||
self._strategies = {}
|
||||
self.strategy = self.__init_strategy(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)
|
||||
|
||||
|
||||
class MapperOption(object):
|
||||
"""Describe a modification to a Query."""
|
||||
|
||||
propagate_to_loaders = False
|
||||
"""if True, indicate this option should be carried along
|
||||
Query object generated by scalar or object lazy loaders.
|
||||
"""
|
||||
|
||||
def process_query(self, query):
|
||||
pass
|
||||
|
||||
def process_query_conditionally(self, query):
|
||||
"""same as process_query(), except that this option may not
|
||||
apply to the given query.
|
||||
|
||||
Used when secondary loaders resend existing options to a new
|
||||
Query."""
|
||||
|
||||
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 = self._process_paths(query, raiseerr)
|
||||
if paths:
|
||||
self.process_query_property(query, paths)
|
||||
|
||||
def process_query_property(self, query, paths):
|
||||
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._parentmapper.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 orm_util._is_aliased_class(mapper):
|
||||
searchfor = mapper
|
||||
else:
|
||||
searchfor = orm_util._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):
|
||||
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 _process_paths(self, query, raiseerr):
|
||||
"""reconcile the 'key' for this PropertyOption with
|
||||
the current path and entities of the query.
|
||||
|
||||
Return a list of affected paths.
|
||||
|
||||
"""
|
||||
path = orm_util.PathRegistry.root
|
||||
entity = None
|
||||
paths = []
|
||||
no_result = []
|
||||
|
||||
# _current_path implies we're in a
|
||||
# secondary load with an existing path
|
||||
current_path = list(query._current_path.path)
|
||||
|
||||
tokens = deque(self.key)
|
||||
while tokens:
|
||||
token = tokens.popleft()
|
||||
if isinstance(token, basestring):
|
||||
# wildcard token
|
||||
if token.endswith(':*'):
|
||||
return [path.token(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].key == token:
|
||||
current_path = current_path[2:]
|
||||
continue
|
||||
else:
|
||||
return no_result
|
||||
|
||||
if not entity:
|
||||
entity = self._find_entity_basestring(
|
||||
query,
|
||||
token,
|
||||
raiseerr)
|
||||
if entity is None:
|
||||
return no_result
|
||||
path_element = entity.entity_zero
|
||||
mapper = entity.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 no_result
|
||||
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]:
|
||||
current_path = current_path[2:]
|
||||
continue
|
||||
else:
|
||||
return no_result
|
||||
|
||||
if not entity:
|
||||
entity = self._find_entity_prop_comparator(
|
||||
query,
|
||||
prop.key,
|
||||
token._parententity,
|
||||
raiseerr)
|
||||
if not entity:
|
||||
return no_result
|
||||
|
||||
path_element = entity.entity_zero
|
||||
mapper = entity.mapper
|
||||
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 = path[path_element][prop]
|
||||
|
||||
paths.append(path)
|
||||
|
||||
if getattr(token, '_of_type', None):
|
||||
ac = token._of_type
|
||||
ext_info = inspect(ac)
|
||||
path_element = mapper = 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)
|
||||
ext_info = inspect(ac)
|
||||
path.set(query, "path_with_polymorphic", ext_info)
|
||||
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 no_result
|
||||
|
||||
return paths
|
||||
|
||||
|
||||
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):
|
||||
strategy = self.get_strategy_class()
|
||||
if self.chained:
|
||||
for path in paths:
|
||||
path.set(
|
||||
query,
|
||||
"loaderstrategy",
|
||||
strategy
|
||||
)
|
||||
else:
|
||||
paths[-1].set(
|
||||
query,
|
||||
"loaderstrategy",
|
||||
strategy
|
||||
)
|
||||
|
||||
def get_strategy_class(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class LoaderStrategy(object):
|
||||
"""Describe the loading behavior of a StrategizedProperty object.
|
||||
|
||||
The ``LoaderStrategy`` interacts with the querying process in three
|
||||
ways:
|
||||
|
||||
* it controls the configuration of the ``InstrumentedAttribute``
|
||||
placed on a class to handle the behavior of the attribute. this
|
||||
may involve setting up class-level callable functions to fire
|
||||
off a select operation when the attribute is first accessed
|
||||
(i.e. a lazy load)
|
||||
|
||||
* it processes the ``QueryContext`` at statement construction time,
|
||||
where it can modify the SQL statement that is being produced.
|
||||
Simple column attributes may add their represented column to the
|
||||
list of selected columns, *eager loading* properties may add
|
||||
``LEFT OUTER JOIN`` clauses to the statement.
|
||||
|
||||
* It produces "row processor" functions at result fetching time.
|
||||
These "row processor" functions populate a particular attribute
|
||||
on a particular mapped instance.
|
||||
|
||||
"""
|
||||
def __init__(self, parent):
|
||||
self.parent_property = parent
|
||||
self.is_class_level = False
|
||||
self.parent = self.parent_property.parent
|
||||
self.key = self.parent_property.key
|
||||
|
||||
def init_class_attribute(self, mapper):
|
||||
pass
|
||||
|
||||
def setup_query(self, context, entity, path, adapter, **kwargs):
|
||||
pass
|
||||
|
||||
def create_row_processor(self, context, path, mapper,
|
||||
row, adapter):
|
||||
"""Return row processing functions which fulfill the contract
|
||||
specified by MapperProperty.create_row_processor.
|
||||
|
||||
StrategizedProperty delegates its create_row_processor method
|
||||
directly to this method. """
|
||||
|
||||
return None, None, None
|
||||
|
||||
def __str__(self):
|
||||
return str(self.parent_property)
|
||||
Vendored
+606
@@ -0,0 +1,606 @@
|
||||
# orm/loading.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
|
||||
|
||||
"""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 __future__ import absolute_import
|
||||
|
||||
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
|
||||
sessionlib = util.importlater("sqlalchemy.orm", "session")
|
||||
|
||||
_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 = filtered and len(query._entities) == 1
|
||||
|
||||
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].mapper.dispatch.append_result
|
||||
|
||||
(process, labels) = \
|
||||
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(
|
||||
context.progress.items(),
|
||||
session.identity_map
|
||||
)
|
||||
|
||||
for state, (dict_, attrs) in context.partials.iteritems():
|
||||
state._commit(dict_, attrs)
|
||||
|
||||
for row in rows:
|
||||
yield row
|
||||
|
||||
if not query._yield_per:
|
||||
break
|
||||
|
||||
|
||||
def merge_result(query, iterator, load=True):
|
||||
"""Merge a result into this :class:`.Query` object's Session."""
|
||||
|
||||
from . import query as querylib
|
||||
|
||||
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."""
|
||||
|
||||
lockmode = lockmode or query._lockmode
|
||||
|
||||
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:
|
||||
q._lockmode = lockmode
|
||||
q._get_options(
|
||||
populate_existing=bool(refresh_state),
|
||||
version_check=(lockmode is not None),
|
||||
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.itervalues():
|
||||
|
||||
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 = sessionlib._state_session(state)
|
||||
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)
|
||||
Vendored
+2289
File diff suppressed because it is too large
Load Diff
Vendored
+1037
File diff suppressed because it is too large
Load Diff
Vendored
+1322
File diff suppressed because it is too large
Load Diff
Vendored
+3272
File diff suppressed because it is too large
Load Diff
+908
@@ -0,0 +1,908 @@
|
||||
# orm/relationships.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
|
||||
|
||||
"""Heuristics related to join conditions as used in
|
||||
:func:`.relationship`.
|
||||
|
||||
Provides the :class:`.JoinCondition` object, which encapsulates
|
||||
SQL annotation and aliasing behavior focused on the `primaryjoin`
|
||||
and `secondaryjoin` aspects of :func:`.relationship`.
|
||||
|
||||
"""
|
||||
|
||||
from .. import sql, util, exc as sa_exc, schema
|
||||
from ..sql.util import (
|
||||
ClauseAdapter,
|
||||
join_condition, _shallow_annotate, visit_binary_product,
|
||||
_deep_deannotate, find_tables
|
||||
)
|
||||
from ..sql import operators, expression, visitors
|
||||
from .interfaces import MANYTOMANY, MANYTOONE, ONETOMANY
|
||||
|
||||
|
||||
def remote(expr):
|
||||
"""Annotate a portion of a primaryjoin expression
|
||||
with a 'remote' annotation.
|
||||
|
||||
See the section :ref:`relationship_custom_foreign` for a
|
||||
description of use.
|
||||
|
||||
.. versionadded:: 0.8
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`relationship_custom_foreign`
|
||||
|
||||
:func:`.foreign`
|
||||
|
||||
"""
|
||||
return _annotate_columns(expression._clause_element_as_expr(expr),
|
||||
{"remote": True})
|
||||
|
||||
|
||||
def foreign(expr):
|
||||
"""Annotate a portion of a primaryjoin expression
|
||||
with a 'foreign' annotation.
|
||||
|
||||
See the section :ref:`relationship_custom_foreign` for a
|
||||
description of use.
|
||||
|
||||
.. versionadded:: 0.8
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`relationship_custom_foreign`
|
||||
|
||||
:func:`.remote`
|
||||
|
||||
"""
|
||||
|
||||
return _annotate_columns(expression._clause_element_as_expr(expr),
|
||||
{"foreign": True})
|
||||
|
||||
|
||||
def _annotate_columns(element, annotations):
|
||||
def clone(elem):
|
||||
if isinstance(elem, expression.ColumnClause):
|
||||
elem = elem._annotate(annotations.copy())
|
||||
elem._copy_internals(clone=clone)
|
||||
return elem
|
||||
|
||||
if element is not None:
|
||||
element = clone(element)
|
||||
return element
|
||||
|
||||
|
||||
class JoinCondition(object):
|
||||
def __init__(self,
|
||||
parent_selectable,
|
||||
child_selectable,
|
||||
parent_local_selectable,
|
||||
child_local_selectable,
|
||||
primaryjoin=None,
|
||||
secondary=None,
|
||||
secondaryjoin=None,
|
||||
parent_equivalents=None,
|
||||
child_equivalents=None,
|
||||
consider_as_foreign_keys=None,
|
||||
local_remote_pairs=None,
|
||||
remote_side=None,
|
||||
self_referential=False,
|
||||
prop=None,
|
||||
support_sync=True,
|
||||
can_be_synced_fn=lambda *c: True
|
||||
):
|
||||
self.parent_selectable = parent_selectable
|
||||
self.parent_local_selectable = parent_local_selectable
|
||||
self.child_selectable = child_selectable
|
||||
self.child_local_selectable = child_local_selectable
|
||||
self.parent_equivalents = parent_equivalents
|
||||
self.child_equivalents = child_equivalents
|
||||
self.primaryjoin = primaryjoin
|
||||
self.secondaryjoin = secondaryjoin
|
||||
self.secondary = secondary
|
||||
self.consider_as_foreign_keys = consider_as_foreign_keys
|
||||
self._local_remote_pairs = local_remote_pairs
|
||||
self._remote_side = remote_side
|
||||
self.prop = prop
|
||||
self.self_referential = self_referential
|
||||
self.support_sync = support_sync
|
||||
self.can_be_synced_fn = can_be_synced_fn
|
||||
self._determine_joins()
|
||||
self._annotate_fks()
|
||||
self._annotate_remote()
|
||||
self._annotate_local()
|
||||
self._setup_pairs()
|
||||
self._check_foreign_cols(self.primaryjoin, True)
|
||||
if self.secondaryjoin is not None:
|
||||
self._check_foreign_cols(self.secondaryjoin, False)
|
||||
self._determine_direction()
|
||||
self._check_remote_side()
|
||||
self._log_joins()
|
||||
|
||||
def _log_joins(self):
|
||||
if self.prop is None:
|
||||
return
|
||||
log = self.prop.logger
|
||||
log.info('%s setup primary join %s', self.prop,
|
||||
self.primaryjoin)
|
||||
log.info('%s setup secondary join %s', self.prop,
|
||||
self.secondaryjoin)
|
||||
log.info('%s synchronize pairs [%s]', self.prop,
|
||||
','.join('(%s => %s)' % (l, r) for (l, r) in
|
||||
self.synchronize_pairs))
|
||||
log.info('%s secondary synchronize pairs [%s]', self.prop,
|
||||
','.join('(%s => %s)' % (l, r) for (l, r) in
|
||||
self.secondary_synchronize_pairs or []))
|
||||
log.info('%s local/remote pairs [%s]', self.prop,
|
||||
','.join('(%s / %s)' % (l, r) for (l, r) in
|
||||
self.local_remote_pairs))
|
||||
log.info('%s remote columns [%s]', self.prop,
|
||||
','.join('%s' % col for col in self.remote_columns)
|
||||
)
|
||||
log.info('%s local columns [%s]', self.prop,
|
||||
','.join('%s' % col for col in self.local_columns)
|
||||
)
|
||||
log.info('%s relationship direction %s', self.prop,
|
||||
self.direction)
|
||||
|
||||
def _determine_joins(self):
|
||||
"""Determine the 'primaryjoin' and 'secondaryjoin' attributes,
|
||||
if not passed to the constructor already.
|
||||
|
||||
This is based on analysis of the foreign key relationships
|
||||
between the parent and target mapped selectables.
|
||||
|
||||
"""
|
||||
if self.secondaryjoin is not None and self.secondary is None:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Property %s specified with secondary "
|
||||
"join condition but "
|
||||
"no secondary argument" % self.prop)
|
||||
|
||||
# find a join between the given mapper's mapped table and
|
||||
# the given table. will try the mapper's local table first
|
||||
# for more specificity, then if not found will try the more
|
||||
# general mapped table, which in the case of inheritance is
|
||||
# a join.
|
||||
try:
|
||||
consider_as_foreign_keys = self.consider_as_foreign_keys or None
|
||||
if self.secondary is not None:
|
||||
if self.secondaryjoin is None:
|
||||
self.secondaryjoin = \
|
||||
join_condition(
|
||||
self.child_selectable,
|
||||
self.secondary,
|
||||
a_subset=self.child_local_selectable,
|
||||
consider_as_foreign_keys=consider_as_foreign_keys
|
||||
)
|
||||
if self.primaryjoin is None:
|
||||
self.primaryjoin = \
|
||||
join_condition(
|
||||
self.parent_selectable,
|
||||
self.secondary,
|
||||
a_subset=self.parent_local_selectable,
|
||||
consider_as_foreign_keys=consider_as_foreign_keys
|
||||
)
|
||||
else:
|
||||
if self.primaryjoin is None:
|
||||
self.primaryjoin = \
|
||||
join_condition(
|
||||
self.parent_selectable,
|
||||
self.child_selectable,
|
||||
a_subset=self.parent_local_selectable,
|
||||
consider_as_foreign_keys=consider_as_foreign_keys
|
||||
)
|
||||
except sa_exc.NoForeignKeysError:
|
||||
if self.secondary is not None:
|
||||
raise sa_exc.NoForeignKeysError("Could not determine join "
|
||||
"condition between parent/child tables on "
|
||||
"relationship %s - there are no foreign keys "
|
||||
"linking these tables via secondary table '%s'. "
|
||||
"Ensure that referencing columns are associated "
|
||||
"with a ForeignKey or ForeignKeyConstraint, or "
|
||||
"specify 'primaryjoin' and 'secondaryjoin' "
|
||||
"expressions."
|
||||
% (self.prop, self.secondary))
|
||||
else:
|
||||
raise sa_exc.NoForeignKeysError("Could not determine join "
|
||||
"condition between parent/child tables on "
|
||||
"relationship %s - there are no foreign keys "
|
||||
"linking these tables. "
|
||||
"Ensure that referencing columns are associated "
|
||||
"with a ForeignKey or ForeignKeyConstraint, or "
|
||||
"specify a 'primaryjoin' expression."
|
||||
% self.prop)
|
||||
except sa_exc.AmbiguousForeignKeysError:
|
||||
if self.secondary is not None:
|
||||
raise sa_exc.AmbiguousForeignKeysError(
|
||||
"Could not determine join "
|
||||
"condition between parent/child tables on "
|
||||
"relationship %s - there are multiple foreign key "
|
||||
"paths linking the tables via secondary table '%s'. "
|
||||
"Specify the 'foreign_keys' "
|
||||
"argument, providing a list of those columns which "
|
||||
"should be counted as containing a foreign key "
|
||||
"reference from the secondary table to each of the "
|
||||
"parent and child tables."
|
||||
% (self.prop, self.secondary))
|
||||
else:
|
||||
raise sa_exc.AmbiguousForeignKeysError(
|
||||
"Could not determine join "
|
||||
"condition between parent/child tables on "
|
||||
"relationship %s - there are multiple foreign key "
|
||||
"paths linking the tables. Specify the "
|
||||
"'foreign_keys' argument, providing a list of those "
|
||||
"columns which should be counted as containing a "
|
||||
"foreign key reference to the parent table."
|
||||
% self.prop)
|
||||
|
||||
@property
|
||||
def primaryjoin_minus_local(self):
|
||||
return _deep_deannotate(self.primaryjoin, values=("local", "remote"))
|
||||
|
||||
@property
|
||||
def secondaryjoin_minus_local(self):
|
||||
return _deep_deannotate(self.secondaryjoin, values=("local", "remote"))
|
||||
|
||||
@util.memoized_property
|
||||
def primaryjoin_reverse_remote(self):
|
||||
"""Return the primaryjoin condition suitable for the
|
||||
"reverse" direction.
|
||||
|
||||
If the primaryjoin was delivered here with pre-existing
|
||||
"remote" annotations, the local/remote annotations
|
||||
are reversed. Otherwise, the local/remote annotations
|
||||
are removed.
|
||||
|
||||
"""
|
||||
if self._has_remote_annotations:
|
||||
def replace(element):
|
||||
if "remote" in element._annotations:
|
||||
v = element._annotations.copy()
|
||||
del v['remote']
|
||||
v['local'] = True
|
||||
return element._with_annotations(v)
|
||||
elif "local" in element._annotations:
|
||||
v = element._annotations.copy()
|
||||
del v['local']
|
||||
v['remote'] = True
|
||||
return element._with_annotations(v)
|
||||
return visitors.replacement_traverse(
|
||||
self.primaryjoin, {}, replace)
|
||||
else:
|
||||
if self._has_foreign_annotations:
|
||||
# TODO: coverage
|
||||
return _deep_deannotate(self.primaryjoin,
|
||||
values=("local", "remote"))
|
||||
else:
|
||||
return _deep_deannotate(self.primaryjoin)
|
||||
|
||||
def _has_annotation(self, clause, annotation):
|
||||
for col in visitors.iterate(clause, {}):
|
||||
if annotation in col._annotations:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@util.memoized_property
|
||||
def _has_foreign_annotations(self):
|
||||
return self._has_annotation(self.primaryjoin, "foreign")
|
||||
|
||||
@util.memoized_property
|
||||
def _has_remote_annotations(self):
|
||||
return self._has_annotation(self.primaryjoin, "remote")
|
||||
|
||||
def _annotate_fks(self):
|
||||
"""Annotate the primaryjoin and secondaryjoin
|
||||
structures with 'foreign' annotations marking columns
|
||||
considered as foreign.
|
||||
|
||||
"""
|
||||
if self._has_foreign_annotations:
|
||||
return
|
||||
|
||||
if self.consider_as_foreign_keys:
|
||||
self._annotate_from_fk_list()
|
||||
else:
|
||||
self._annotate_present_fks()
|
||||
|
||||
def _annotate_from_fk_list(self):
|
||||
def check_fk(col):
|
||||
if col in self.consider_as_foreign_keys:
|
||||
return col._annotate({"foreign": True})
|
||||
self.primaryjoin = visitors.replacement_traverse(
|
||||
self.primaryjoin,
|
||||
{},
|
||||
check_fk
|
||||
)
|
||||
if self.secondaryjoin is not None:
|
||||
self.secondaryjoin = visitors.replacement_traverse(
|
||||
self.secondaryjoin,
|
||||
{},
|
||||
check_fk
|
||||
)
|
||||
|
||||
def _annotate_present_fks(self):
|
||||
if self.secondary is not None:
|
||||
secondarycols = util.column_set(self.secondary.c)
|
||||
else:
|
||||
secondarycols = set()
|
||||
|
||||
def is_foreign(a, b):
|
||||
if isinstance(a, schema.Column) and \
|
||||
isinstance(b, schema.Column):
|
||||
if a.references(b):
|
||||
return a
|
||||
elif b.references(a):
|
||||
return b
|
||||
|
||||
if secondarycols:
|
||||
if a in secondarycols and b not in secondarycols:
|
||||
return a
|
||||
elif b in secondarycols and a not in secondarycols:
|
||||
return b
|
||||
|
||||
def visit_binary(binary):
|
||||
if not isinstance(binary.left, sql.ColumnElement) or \
|
||||
not isinstance(binary.right, sql.ColumnElement):
|
||||
return
|
||||
|
||||
if "foreign" not in binary.left._annotations and \
|
||||
"foreign" not in binary.right._annotations:
|
||||
col = is_foreign(binary.left, binary.right)
|
||||
if col is not None:
|
||||
if col.compare(binary.left):
|
||||
binary.left = binary.left._annotate(
|
||||
{"foreign": True})
|
||||
elif col.compare(binary.right):
|
||||
binary.right = binary.right._annotate(
|
||||
{"foreign": True})
|
||||
|
||||
self.primaryjoin = visitors.cloned_traverse(
|
||||
self.primaryjoin,
|
||||
{},
|
||||
{"binary": visit_binary}
|
||||
)
|
||||
if self.secondaryjoin is not None:
|
||||
self.secondaryjoin = visitors.cloned_traverse(
|
||||
self.secondaryjoin,
|
||||
{},
|
||||
{"binary": visit_binary}
|
||||
)
|
||||
|
||||
def _refers_to_parent_table(self):
|
||||
"""Return True if the join condition contains column
|
||||
comparisons where both columns are in both tables.
|
||||
|
||||
"""
|
||||
pt = self.parent_selectable
|
||||
mt = self.child_selectable
|
||||
result = [False]
|
||||
|
||||
def visit_binary(binary):
|
||||
c, f = binary.left, binary.right
|
||||
if (
|
||||
isinstance(c, expression.ColumnClause) and \
|
||||
isinstance(f, expression.ColumnClause) and \
|
||||
pt.is_derived_from(c.table) and \
|
||||
pt.is_derived_from(f.table) and \
|
||||
mt.is_derived_from(c.table) and \
|
||||
mt.is_derived_from(f.table)
|
||||
):
|
||||
result[0] = True
|
||||
visitors.traverse(
|
||||
self.primaryjoin,
|
||||
{},
|
||||
{"binary": visit_binary}
|
||||
)
|
||||
return result[0]
|
||||
|
||||
def _tables_overlap(self):
|
||||
"""Return True if parent/child tables have some overlap."""
|
||||
|
||||
return bool(
|
||||
set(find_tables(self.parent_selectable)).intersection(
|
||||
find_tables(self.child_selectable)
|
||||
)
|
||||
)
|
||||
|
||||
def _annotate_remote(self):
|
||||
"""Annotate the primaryjoin and secondaryjoin
|
||||
structures with 'remote' annotations marking columns
|
||||
considered as part of the 'remote' side.
|
||||
|
||||
"""
|
||||
if self._has_remote_annotations:
|
||||
return
|
||||
|
||||
if self.secondary is not None:
|
||||
self._annotate_remote_secondary()
|
||||
elif self._local_remote_pairs or self._remote_side:
|
||||
self._annotate_remote_from_args()
|
||||
elif self._refers_to_parent_table():
|
||||
self._annotate_selfref(lambda col: "foreign" in col._annotations)
|
||||
elif self._tables_overlap():
|
||||
self._annotate_remote_with_overlap()
|
||||
else:
|
||||
self._annotate_remote_distinct_selectables()
|
||||
|
||||
def _annotate_remote_secondary(self):
|
||||
"""annotate 'remote' in primaryjoin, secondaryjoin
|
||||
when 'secondary' is present.
|
||||
|
||||
"""
|
||||
def repl(element):
|
||||
if self.secondary.c.contains_column(element):
|
||||
return element._annotate({"remote": True})
|
||||
self.primaryjoin = visitors.replacement_traverse(
|
||||
self.primaryjoin, {}, repl)
|
||||
self.secondaryjoin = visitors.replacement_traverse(
|
||||
self.secondaryjoin, {}, repl)
|
||||
|
||||
def _annotate_selfref(self, fn):
|
||||
"""annotate 'remote' in primaryjoin, secondaryjoin
|
||||
when the relationship is detected as self-referential.
|
||||
|
||||
"""
|
||||
def visit_binary(binary):
|
||||
equated = binary.left.compare(binary.right)
|
||||
if isinstance(binary.left, expression.ColumnClause) and \
|
||||
isinstance(binary.right, expression.ColumnClause):
|
||||
# assume one to many - FKs are "remote"
|
||||
if fn(binary.left):
|
||||
binary.left = binary.left._annotate({"remote": True})
|
||||
if fn(binary.right) and not equated:
|
||||
binary.right = binary.right._annotate(
|
||||
{"remote": True})
|
||||
else:
|
||||
self._warn_non_column_elements()
|
||||
|
||||
self.primaryjoin = visitors.cloned_traverse(
|
||||
self.primaryjoin, {},
|
||||
{"binary": visit_binary})
|
||||
|
||||
def _annotate_remote_from_args(self):
|
||||
"""annotate 'remote' in primaryjoin, secondaryjoin
|
||||
when the 'remote_side' or '_local_remote_pairs'
|
||||
arguments are used.
|
||||
|
||||
"""
|
||||
if self._local_remote_pairs:
|
||||
if self._remote_side:
|
||||
raise sa_exc.ArgumentError(
|
||||
"remote_side argument is redundant "
|
||||
"against more detailed _local_remote_side "
|
||||
"argument.")
|
||||
|
||||
remote_side = [r for (l, r) in self._local_remote_pairs]
|
||||
else:
|
||||
remote_side = self._remote_side
|
||||
|
||||
if self._refers_to_parent_table():
|
||||
self._annotate_selfref(lambda col: col in remote_side)
|
||||
else:
|
||||
def repl(element):
|
||||
if element in remote_side:
|
||||
return element._annotate({"remote": True})
|
||||
self.primaryjoin = visitors.replacement_traverse(
|
||||
self.primaryjoin, {}, repl)
|
||||
|
||||
def _annotate_remote_with_overlap(self):
|
||||
"""annotate 'remote' in primaryjoin, secondaryjoin
|
||||
when the parent/child tables have some set of
|
||||
tables in common, though is not a fully self-referential
|
||||
relationship.
|
||||
|
||||
"""
|
||||
def visit_binary(binary):
|
||||
binary.left, binary.right = proc_left_right(binary.left,
|
||||
binary.right)
|
||||
binary.right, binary.left = proc_left_right(binary.right,
|
||||
binary.left)
|
||||
|
||||
def proc_left_right(left, right):
|
||||
if isinstance(left, expression.ColumnClause) and \
|
||||
isinstance(right, expression.ColumnClause):
|
||||
if self.child_selectable.c.contains_column(right) and \
|
||||
self.parent_selectable.c.contains_column(left):
|
||||
right = right._annotate({"remote": True})
|
||||
else:
|
||||
self._warn_non_column_elements()
|
||||
|
||||
return left, right
|
||||
|
||||
self.primaryjoin = visitors.cloned_traverse(
|
||||
self.primaryjoin, {},
|
||||
{"binary": visit_binary})
|
||||
|
||||
def _annotate_remote_distinct_selectables(self):
|
||||
"""annotate 'remote' in primaryjoin, secondaryjoin
|
||||
when the parent/child tables are entirely
|
||||
separate.
|
||||
|
||||
"""
|
||||
def repl(element):
|
||||
if self.child_selectable.c.contains_column(element) and \
|
||||
(
|
||||
not self.parent_local_selectable.c.\
|
||||
contains_column(element)
|
||||
or self.child_local_selectable.c.\
|
||||
contains_column(element)):
|
||||
return element._annotate({"remote": True})
|
||||
self.primaryjoin = visitors.replacement_traverse(
|
||||
self.primaryjoin, {}, repl)
|
||||
|
||||
def _warn_non_column_elements(self):
|
||||
util.warn(
|
||||
"Non-simple column elements in primary "
|
||||
"join condition for property %s - consider using "
|
||||
"remote() annotations to mark the remote side."
|
||||
% self.prop
|
||||
)
|
||||
|
||||
def _annotate_local(self):
|
||||
"""Annotate the primaryjoin and secondaryjoin
|
||||
structures with 'local' annotations.
|
||||
|
||||
This annotates all column elements found
|
||||
simultaneously in the parent table
|
||||
and the join condition that don't have a
|
||||
'remote' annotation set up from
|
||||
_annotate_remote() or user-defined.
|
||||
|
||||
"""
|
||||
if self._has_annotation(self.primaryjoin, "local"):
|
||||
return
|
||||
|
||||
if self._local_remote_pairs:
|
||||
local_side = util.column_set([l for (l, r)
|
||||
in self._local_remote_pairs])
|
||||
else:
|
||||
local_side = util.column_set(self.parent_selectable.c)
|
||||
|
||||
def locals_(elem):
|
||||
if "remote" not in elem._annotations and \
|
||||
elem in local_side:
|
||||
return elem._annotate({"local": True})
|
||||
self.primaryjoin = visitors.replacement_traverse(
|
||||
self.primaryjoin, {}, locals_
|
||||
)
|
||||
|
||||
def _check_remote_side(self):
|
||||
if not self.local_remote_pairs:
|
||||
raise sa_exc.ArgumentError('Relationship %s could '
|
||||
'not determine any unambiguous local/remote column '
|
||||
'pairs based on join condition and remote_side '
|
||||
'arguments. '
|
||||
'Consider using the remote() annotation to '
|
||||
'accurately mark those elements of the join '
|
||||
'condition that are on the remote side of '
|
||||
'the relationship.'
|
||||
% (self.prop, ))
|
||||
|
||||
def _check_foreign_cols(self, join_condition, primary):
|
||||
"""Check the foreign key columns collected and emit error
|
||||
messages."""
|
||||
|
||||
can_sync = False
|
||||
|
||||
foreign_cols = self._gather_columns_with_annotation(
|
||||
join_condition, "foreign")
|
||||
|
||||
has_foreign = bool(foreign_cols)
|
||||
|
||||
if primary:
|
||||
can_sync = bool(self.synchronize_pairs)
|
||||
else:
|
||||
can_sync = bool(self.secondary_synchronize_pairs)
|
||||
|
||||
if self.support_sync and can_sync or \
|
||||
(not self.support_sync and has_foreign):
|
||||
return
|
||||
|
||||
# from here below is just determining the best error message
|
||||
# to report. Check for a join condition using any operator
|
||||
# (not just ==), perhaps they need to turn on "viewonly=True".
|
||||
if self.support_sync and has_foreign and not can_sync:
|
||||
err = "Could not locate any simple equality expressions "\
|
||||
"involving locally mapped foreign key columns for "\
|
||||
"%s join condition "\
|
||||
"'%s' on relationship %s." % (
|
||||
primary and 'primary' or 'secondary',
|
||||
join_condition,
|
||||
self.prop
|
||||
)
|
||||
err += \
|
||||
" Ensure that referencing columns are associated "\
|
||||
"with a ForeignKey or ForeignKeyConstraint, or are "\
|
||||
"annotated in the join condition with the foreign() "\
|
||||
"annotation. To allow comparison operators other than "\
|
||||
"'==', the relationship can be marked as viewonly=True."
|
||||
|
||||
raise sa_exc.ArgumentError(err)
|
||||
else:
|
||||
err = "Could not locate any relevant foreign key columns "\
|
||||
"for %s join condition '%s' on relationship %s." % (
|
||||
primary and 'primary' or 'secondary',
|
||||
join_condition,
|
||||
self.prop
|
||||
)
|
||||
err += \
|
||||
' Ensure that referencing columns are associated '\
|
||||
'with a ForeignKey or ForeignKeyConstraint, or are '\
|
||||
'annotated in the join condition with the foreign() '\
|
||||
'annotation.'
|
||||
raise sa_exc.ArgumentError(err)
|
||||
|
||||
def _determine_direction(self):
|
||||
"""Determine if this relationship is one to many, many to one,
|
||||
many to many.
|
||||
|
||||
"""
|
||||
if self.secondaryjoin is not None:
|
||||
self.direction = MANYTOMANY
|
||||
else:
|
||||
parentcols = util.column_set(self.parent_selectable.c)
|
||||
targetcols = util.column_set(self.child_selectable.c)
|
||||
|
||||
# fk collection which suggests ONETOMANY.
|
||||
onetomany_fk = targetcols.intersection(
|
||||
self.foreign_key_columns)
|
||||
|
||||
# fk collection which suggests MANYTOONE.
|
||||
|
||||
manytoone_fk = parentcols.intersection(
|
||||
self.foreign_key_columns)
|
||||
|
||||
if onetomany_fk and manytoone_fk:
|
||||
# fks on both sides. test for overlap of local/remote
|
||||
# with foreign key
|
||||
self_equated = self.remote_columns.intersection(
|
||||
self.local_columns
|
||||
)
|
||||
onetomany_local = self.remote_columns.\
|
||||
intersection(self.foreign_key_columns).\
|
||||
difference(self_equated)
|
||||
manytoone_local = self.local_columns.\
|
||||
intersection(self.foreign_key_columns).\
|
||||
difference(self_equated)
|
||||
if onetomany_local and not manytoone_local:
|
||||
self.direction = ONETOMANY
|
||||
elif manytoone_local and not onetomany_local:
|
||||
self.direction = MANYTOONE
|
||||
else:
|
||||
raise sa_exc.ArgumentError(
|
||||
"Can't determine relationship"
|
||||
" direction for relationship '%s' - foreign "
|
||||
"key columns within the join condition are present "
|
||||
"in both the parent and the child's mapped tables. "
|
||||
"Ensure that only those columns referring "
|
||||
"to a parent column are marked as foreign, "
|
||||
"either via the foreign() annotation or "
|
||||
"via the foreign_keys argument." % self.prop)
|
||||
elif onetomany_fk:
|
||||
self.direction = ONETOMANY
|
||||
elif manytoone_fk:
|
||||
self.direction = MANYTOONE
|
||||
else:
|
||||
raise sa_exc.ArgumentError("Can't determine relationship "
|
||||
"direction for relationship '%s' - foreign "
|
||||
"key columns are present in neither the parent "
|
||||
"nor the child's mapped tables" % self.prop)
|
||||
|
||||
def _deannotate_pairs(self, collection):
|
||||
"""provide deannotation for the various lists of
|
||||
pairs, so that using them in hashes doesn't incur
|
||||
high-overhead __eq__() comparisons against
|
||||
original columns mapped.
|
||||
|
||||
"""
|
||||
return [(x._deannotate(), y._deannotate())
|
||||
for x, y in collection]
|
||||
|
||||
def _setup_pairs(self):
|
||||
sync_pairs = []
|
||||
lrp = util.OrderedSet([])
|
||||
secondary_sync_pairs = []
|
||||
|
||||
def go(joincond, collection):
|
||||
def visit_binary(binary, left, right):
|
||||
if "remote" in right._annotations and \
|
||||
"remote" not in left._annotations and \
|
||||
self.can_be_synced_fn(left):
|
||||
lrp.add((left, right))
|
||||
elif "remote" in left._annotations and \
|
||||
"remote" not in right._annotations and \
|
||||
self.can_be_synced_fn(right):
|
||||
lrp.add((right, left))
|
||||
if binary.operator is operators.eq and \
|
||||
self.can_be_synced_fn(left, right):
|
||||
if "foreign" in right._annotations:
|
||||
collection.append((left, right))
|
||||
elif "foreign" in left._annotations:
|
||||
collection.append((right, left))
|
||||
visit_binary_product(visit_binary, joincond)
|
||||
|
||||
for joincond, collection in [
|
||||
(self.primaryjoin, sync_pairs),
|
||||
(self.secondaryjoin, secondary_sync_pairs)
|
||||
]:
|
||||
if joincond is None:
|
||||
continue
|
||||
go(joincond, collection)
|
||||
|
||||
self.local_remote_pairs = self._deannotate_pairs(lrp)
|
||||
self.synchronize_pairs = self._deannotate_pairs(sync_pairs)
|
||||
self.secondary_synchronize_pairs = \
|
||||
self._deannotate_pairs(secondary_sync_pairs)
|
||||
|
||||
@util.memoized_property
|
||||
def remote_columns(self):
|
||||
return self._gather_join_annotations("remote")
|
||||
|
||||
@util.memoized_property
|
||||
def local_columns(self):
|
||||
return self._gather_join_annotations("local")
|
||||
|
||||
@util.memoized_property
|
||||
def foreign_key_columns(self):
|
||||
return self._gather_join_annotations("foreign")
|
||||
|
||||
@util.memoized_property
|
||||
def deannotated_primaryjoin(self):
|
||||
return _deep_deannotate(self.primaryjoin)
|
||||
|
||||
@util.memoized_property
|
||||
def deannotated_secondaryjoin(self):
|
||||
if self.secondaryjoin is not None:
|
||||
return _deep_deannotate(self.secondaryjoin)
|
||||
else:
|
||||
return None
|
||||
|
||||
def _gather_join_annotations(self, annotation):
|
||||
s = set(
|
||||
self._gather_columns_with_annotation(
|
||||
self.primaryjoin, annotation)
|
||||
)
|
||||
if self.secondaryjoin is not None:
|
||||
s.update(
|
||||
self._gather_columns_with_annotation(
|
||||
self.secondaryjoin, annotation)
|
||||
)
|
||||
return set([x._deannotate() for x in s])
|
||||
|
||||
def _gather_columns_with_annotation(self, clause, *annotation):
|
||||
annotation = set(annotation)
|
||||
return set([
|
||||
col for col in visitors.iterate(clause, {})
|
||||
if annotation.issubset(col._annotations)
|
||||
])
|
||||
|
||||
def join_targets(self, source_selectable,
|
||||
dest_selectable,
|
||||
aliased,
|
||||
single_crit=None):
|
||||
"""Given a source and destination selectable, create a
|
||||
join between them.
|
||||
|
||||
This takes into account aliasing the join clause
|
||||
to reference the appropriate corresponding columns
|
||||
in the target objects, as well as the extra child
|
||||
criterion, equivalent column sets, etc.
|
||||
|
||||
"""
|
||||
|
||||
# place a barrier on the destination such that
|
||||
# replacement traversals won't ever dig into it.
|
||||
# its internal structure remains fixed
|
||||
# regardless of context.
|
||||
dest_selectable = _shallow_annotate(
|
||||
dest_selectable,
|
||||
{'no_replacement_traverse': True})
|
||||
|
||||
primaryjoin, secondaryjoin, secondary = self.primaryjoin, \
|
||||
self.secondaryjoin, self.secondary
|
||||
|
||||
# adjust the join condition for single table inheritance,
|
||||
# in the case that the join is to a subclass
|
||||
# this is analogous to the
|
||||
# "_adjust_for_single_table_inheritance()" method in Query.
|
||||
|
||||
if single_crit is not None:
|
||||
if secondaryjoin is not None:
|
||||
secondaryjoin = secondaryjoin & single_crit
|
||||
else:
|
||||
primaryjoin = primaryjoin & single_crit
|
||||
|
||||
if aliased:
|
||||
if secondary is not None:
|
||||
secondary = secondary.alias()
|
||||
primary_aliasizer = ClauseAdapter(secondary)
|
||||
secondary_aliasizer = \
|
||||
ClauseAdapter(dest_selectable,
|
||||
equivalents=self.child_equivalents).\
|
||||
chain(primary_aliasizer)
|
||||
if source_selectable is not None:
|
||||
primary_aliasizer = \
|
||||
ClauseAdapter(secondary).\
|
||||
chain(ClauseAdapter(source_selectable,
|
||||
equivalents=self.parent_equivalents))
|
||||
secondaryjoin = \
|
||||
secondary_aliasizer.traverse(secondaryjoin)
|
||||
else:
|
||||
primary_aliasizer = ClauseAdapter(dest_selectable,
|
||||
exclude_fn=_ColInAnnotations("local"),
|
||||
equivalents=self.child_equivalents)
|
||||
if source_selectable is not None:
|
||||
primary_aliasizer.chain(
|
||||
ClauseAdapter(source_selectable,
|
||||
exclude_fn=_ColInAnnotations("remote"),
|
||||
equivalents=self.parent_equivalents))
|
||||
secondary_aliasizer = None
|
||||
|
||||
primaryjoin = primary_aliasizer.traverse(primaryjoin)
|
||||
target_adapter = secondary_aliasizer or primary_aliasizer
|
||||
target_adapter.exclude_fn = None
|
||||
else:
|
||||
target_adapter = None
|
||||
return primaryjoin, secondaryjoin, secondary, \
|
||||
target_adapter, dest_selectable
|
||||
|
||||
def create_lazy_clause(self, reverse_direction=False):
|
||||
binds = util.column_dict()
|
||||
lookup = util.column_dict()
|
||||
equated_columns = util.column_dict()
|
||||
|
||||
if reverse_direction and self.secondaryjoin is None:
|
||||
for l, r in self.local_remote_pairs:
|
||||
_list = lookup.setdefault(r, [])
|
||||
_list.append((r, l))
|
||||
equated_columns[l] = r
|
||||
else:
|
||||
for l, r in self.local_remote_pairs:
|
||||
_list = lookup.setdefault(l, [])
|
||||
_list.append((l, r))
|
||||
equated_columns[r] = l
|
||||
|
||||
def col_to_bind(col):
|
||||
if col in lookup:
|
||||
for tobind, equated in lookup[col]:
|
||||
if equated in binds:
|
||||
return None
|
||||
if col not in binds:
|
||||
binds[col] = sql.bindparam(
|
||||
None, None, type_=col.type, unique=True)
|
||||
return binds[col]
|
||||
return None
|
||||
|
||||
lazywhere = self.deannotated_primaryjoin
|
||||
|
||||
if self.deannotated_secondaryjoin is None or not reverse_direction:
|
||||
lazywhere = visitors.replacement_traverse(
|
||||
lazywhere, {}, col_to_bind)
|
||||
|
||||
if self.deannotated_secondaryjoin is not None:
|
||||
secondaryjoin = self.deannotated_secondaryjoin
|
||||
if reverse_direction:
|
||||
secondaryjoin = visitors.replacement_traverse(
|
||||
secondaryjoin, {}, col_to_bind)
|
||||
lazywhere = sql.and_(lazywhere, secondaryjoin)
|
||||
|
||||
bind_to_col = dict((binds[col].key, col) for col in binds)
|
||||
|
||||
return lazywhere, bind_to_col, equated_columns
|
||||
|
||||
class _ColInAnnotations(object):
|
||||
"""Seralizable equivalent to:
|
||||
|
||||
lambda c: "name" in c._annotations
|
||||
"""
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __call__(self, c):
|
||||
return self.name in c._annotations
|
||||
Vendored
+176
@@ -0,0 +1,176 @@
|
||||
# orm/scoping.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 .. import exc as sa_exc
|
||||
from ..util import ScopedRegistry, ThreadLocalRegistry, warn
|
||||
from . import class_mapper, exc as orm_exc
|
||||
from .session import Session
|
||||
|
||||
|
||||
__all__ = ['scoped_session']
|
||||
|
||||
|
||||
class scoped_session(object):
|
||||
"""Provides scoped management of :class:`.Session` objects.
|
||||
|
||||
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, **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(**kw)
|
||||
self.registry.set(sess)
|
||||
return sess
|
||||
else:
|
||||
return self.session_factory(**kw)
|
||||
else:
|
||||
return self.registry()
|
||||
|
||||
def remove(self):
|
||||
"""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 :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. '
|
||||
' configure() can not affect sessions that have '
|
||||
'already been created.')
|
||||
|
||||
self.session_factory.configure(**kwargs)
|
||||
|
||||
def query_property(self, query_cls=None):
|
||||
"""return a class property which produces a :class:`.Query` object
|
||||
against the class and the current :class:`.Session` when called.
|
||||
|
||||
e.g.::
|
||||
|
||||
Session = scoped_session(sessionmaker())
|
||||
|
||||
class MyClass(object):
|
||||
query = Session.query_property()
|
||||
|
||||
# after mappers are defined
|
||||
result = MyClass.query.filter(MyClass.name=='foo').all()
|
||||
|
||||
Produces instances of the session's configured query class by
|
||||
default. To override and use a custom implementation, provide
|
||||
a ``query_cls`` callable. The callable will be invoked with
|
||||
the class's mapper as a positional argument and a session
|
||||
keyword argument.
|
||||
|
||||
There is no limit to the number of query properties placed on
|
||||
a class.
|
||||
|
||||
"""
|
||||
class query(object):
|
||||
def __get__(s, instance, owner):
|
||||
try:
|
||||
mapper = class_mapper(owner)
|
||||
if mapper:
|
||||
if query_cls:
|
||||
# custom query class
|
||||
return query_cls(mapper, session=self.registry())
|
||||
else:
|
||||
# session's configured query class
|
||||
return self.registry().query(mapper)
|
||||
except orm_exc.UnmappedClassError:
|
||||
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(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(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(scoped_session, prop, clslevel(prop))
|
||||
Vendored
+2315
File diff suppressed because it is too large
Load Diff
Vendored
+598
@@ -0,0 +1,598 @@
|
||||
# orm/state.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
|
||||
|
||||
"""Defines instrumentation of instances.
|
||||
|
||||
This module is usually not directly visible to user applications, but
|
||||
defines a large part of the ORM's interactivity.
|
||||
|
||||
"""
|
||||
|
||||
import weakref
|
||||
from .. import util
|
||||
from . import exc as orm_exc, attributes, util as orm_util, interfaces
|
||||
from .attributes import (
|
||||
PASSIVE_NO_RESULT,
|
||||
SQL_OK, NEVER_SET, ATTR_WAS_SET, NO_VALUE,\
|
||||
PASSIVE_NO_INITIALIZE
|
||||
)
|
||||
sessionlib = util.importlater("sqlalchemy.orm", "session")
|
||||
instrumentation = util.importlater("sqlalchemy.orm", "instrumentation")
|
||||
mapperlib = util.importlater("sqlalchemy.orm", "mapperlib")
|
||||
|
||||
|
||||
class InstanceState(interfaces._InspectionAttr):
|
||||
"""tracks state information at the instance level."""
|
||||
|
||||
session_id = None
|
||||
key = None
|
||||
runid = None
|
||||
load_options = util.EMPTY_SET
|
||||
load_path = ()
|
||||
insert_order = 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__
|
||||
self.manager = manager
|
||||
self.obj = weakref.ref(obj, self._cleanup)
|
||||
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
|
||||
def _attached(self):
|
||||
return self.session_id is not None and \
|
||||
self.session_id in sessionlib._sessions
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
"""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_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 = self._strong_obj = None
|
||||
|
||||
def _dispose(self):
|
||||
self._detach()
|
||||
del self.obj
|
||||
|
||||
def _cleanup(self, ref):
|
||||
instance_dict = self._instance_dict()
|
||||
if instance_dict:
|
||||
instance_dict.discard(self)
|
||||
|
||||
self.callables = {}
|
||||
self.session_id = self._strong_obj = None
|
||||
del self.obj
|
||||
|
||||
def obj(self):
|
||||
return None
|
||||
|
||||
@property
|
||||
def dict(self):
|
||||
o = self.obj()
|
||||
if o is not None:
|
||||
return attributes.instance_dict(o)
|
||||
else:
|
||||
return {}
|
||||
|
||||
def _initialize_instance(*mixed, **kwargs):
|
||||
self, instance, args = mixed[0], mixed[1], mixed[2:]
|
||||
manager = self.manager
|
||||
|
||||
manager.dispatch.init(self, args, kwargs)
|
||||
|
||||
try:
|
||||
return manager.original_init(*mixed[1:], **kwargs)
|
||||
except:
|
||||
manager.dispatch.init_failure(self, args, kwargs)
|
||||
raise
|
||||
|
||||
def get_history(self, key, passive):
|
||||
return self.manager[key].impl.get_history(self, self.dict, passive)
|
||||
|
||||
def get_impl(self, key):
|
||||
return self.manager[key].impl
|
||||
|
||||
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(
|
||||
(k, self.__dict__[k]) for k in (
|
||||
'committed_state', '_pending_mutations', 'modified', 'expired',
|
||||
'callables', 'key', 'parents', 'load_options',
|
||||
'class_',
|
||||
) if k in self.__dict__
|
||||
)
|
||||
if self.load_path:
|
||||
d['load_path'] = self.load_path.serialize()
|
||||
|
||||
self.manager.dispatch.pickle(self, d)
|
||||
|
||||
return d
|
||||
|
||||
def __setstate__(self, state):
|
||||
inst = state['instance']
|
||||
if inst is not None:
|
||||
self.obj = weakref.ref(inst, self._cleanup)
|
||||
self.class_ = inst.__class__
|
||||
else:
|
||||
# None being possible here generally new as of 0.7.4
|
||||
# due to storage of state in "parents". "class_"
|
||||
# also new.
|
||||
self.obj = None
|
||||
self.class_ = state['class_']
|
||||
self.manager = manager = instrumentation.manager_of_class(self.class_)
|
||||
if manager is None:
|
||||
raise orm_exc.UnmappedInstanceError(
|
||||
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.committed_state = state.get('committed_state', {})
|
||||
self._pending_mutations = state.get('_pending_mutations', {})
|
||||
self.parents = state.get('parents', {})
|
||||
self.modified = state.get('modified', False)
|
||||
self.expired = state.get('expired', False)
|
||||
self.callables = state.get('callables', {})
|
||||
|
||||
self.__dict__.update([
|
||||
(k, state[k]) for k in (
|
||||
'key', 'load_options',
|
||||
) if k in state
|
||||
])
|
||||
|
||||
if 'load_path' in state:
|
||||
self.load_path = orm_util.PathRegistry.\
|
||||
deserialize(state['load_path'])
|
||||
|
||||
# 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, self)
|
||||
manager.dispatch.unpickle(self, state)
|
||||
|
||||
def _initialize(self, key):
|
||||
"""Set this attribute to an empty value or collection,
|
||||
based on the AttributeImpl in use."""
|
||||
|
||||
self.manager.get_impl(key).initialize(self, self.dict)
|
||||
|
||||
def _reset(self, dict_, key):
|
||||
"""Remove the given attribute and any
|
||||
callables associated with it."""
|
||||
|
||||
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):
|
||||
"""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.
|
||||
|
||||
"""
|
||||
dict_.pop(key, None)
|
||||
self.callables[key] = self
|
||||
|
||||
@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
|
||||
|
||||
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()
|
||||
|
||||
InstanceState._pending_mutations._reset(self)
|
||||
|
||||
# clear out 'parents' collection. not
|
||||
# entirely clear how we can best determine
|
||||
# which to remove, or not.
|
||||
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_):
|
||||
self.callables[key] = self
|
||||
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_mutations', None)
|
||||
|
||||
for key in attribute_names:
|
||||
impl = self.manager[key].impl
|
||||
if impl.accepts_scalar_loader:
|
||||
self.callables[key] = self
|
||||
old = dict_.pop(key, None)
|
||||
if impl.collection and old is not None:
|
||||
impl._invalidate_collection(old)
|
||||
|
||||
self.committed_state.pop(key, None)
|
||||
if pending:
|
||||
pending.pop(key, None)
|
||||
|
||||
self.manager.dispatch.expire(self, attribute_names)
|
||||
|
||||
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 not passive & SQL_OK:
|
||||
return PASSIVE_NO_RESULT
|
||||
|
||||
toload = self.expired_attributes.\
|
||||
intersection(self.unmodified)
|
||||
|
||||
self.manager.deferred_scalar_loader(self, toload)
|
||||
|
||||
# if the loader failed, or this
|
||||
# instance state didn't have an identity,
|
||||
# the attributes still might be in the callables
|
||||
# dict. ensure they are removed.
|
||||
for k in toload.intersection(self.callables):
|
||||
del self.callables[k]
|
||||
|
||||
return ATTR_WAS_SET
|
||||
|
||||
@property
|
||||
def unmodified(self):
|
||||
"""Return the set of keys which have no uncommitted changes"""
|
||||
|
||||
return set(self.manager).difference(self.committed_state)
|
||||
|
||||
def unmodified_intersection(self, keys):
|
||||
"""Return self.unmodified.intersection(keys)."""
|
||||
|
||||
return set(keys).intersection(self.manager).\
|
||||
difference(self.committed_state)
|
||||
|
||||
@property
|
||||
def unloaded(self):
|
||||
"""Return the set of keys which do not have a loaded value.
|
||||
|
||||
This includes expired attributes and any other attribute that
|
||||
was never populated or modified.
|
||||
|
||||
"""
|
||||
return set(self.manager).\
|
||||
difference(self.committed_state).\
|
||||
difference(self.dict)
|
||||
|
||||
@property
|
||||
def expired_attributes(self):
|
||||
"""Return the set of keys which are 'expired' to be loaded by
|
||||
the manager's deferred scalar loader, assuming no pending
|
||||
changes.
|
||||
|
||||
see also the ``unmodified`` collection which is intersected
|
||||
against this set when a refresh operation occurs.
|
||||
|
||||
"""
|
||||
return set([k for k, v in self.callables.items() if v is self])
|
||||
|
||||
def _instance_dict(self):
|
||||
return None
|
||||
|
||||
def _modified_event(self, dict_, attr, previous, collection=False):
|
||||
if attr.key not in self.committed_state:
|
||||
if collection:
|
||||
if previous is NEVER_SET:
|
||||
if attr.key in dict_:
|
||||
previous = dict_[attr.key]
|
||||
|
||||
if previous not in (None, NO_VALUE, NEVER_SET):
|
||||
previous = attr.copy(previous)
|
||||
|
||||
self.committed_state[attr.key] = previous
|
||||
|
||||
# assert self._strong_obj is None or 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)
|
||||
|
||||
# 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)
|
||||
))
|
||||
self.modified = True
|
||||
|
||||
def _commit(self, dict_, keys):
|
||||
"""Commit attributes.
|
||||
|
||||
This is used by a partial-attribute load operation to mark committed
|
||||
those attributes which were refreshed from the database.
|
||||
|
||||
Attributes marked as "expired" can potentially remain "expired" after
|
||||
this step if a value was not populated in state.dict.
|
||||
|
||||
"""
|
||||
for key in keys:
|
||||
self.committed_state.pop(key, None)
|
||||
|
||||
self.expired = False
|
||||
|
||||
for key in set(self.callables).\
|
||||
intersection(keys).\
|
||||
intersection(dict_):
|
||||
del self.callables[key]
|
||||
|
||||
def _commit_all(self, dict_, instance_dict=None):
|
||||
"""commit all attributes unconditionally.
|
||||
|
||||
This is used after a flush() or a full load/refresh
|
||||
to remove all pending state from the instance.
|
||||
|
||||
- all attributes are marked as "committed"
|
||||
- the "strong dirty reference" is removed
|
||||
- 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.
|
||||
|
||||
"""
|
||||
self._commit_all_states([(self, dict_)], instance_dict)
|
||||
|
||||
@classmethod
|
||||
def _commit_all_states(self, iter, instance_dict=None):
|
||||
"""Mass version of commit_all()."""
|
||||
|
||||
for state, dict_ in iter:
|
||||
state.committed_state.clear()
|
||||
InstanceState._pending_mutations._reset(state)
|
||||
|
||||
callables = state.callables
|
||||
for key in list(callables):
|
||||
if key in dict_ and callables[key] is state:
|
||||
del callables[key]
|
||||
|
||||
if instance_dict and state.modified:
|
||||
instance_dict._modified.discard(state)
|
||||
|
||||
state.modified = state.expired = False
|
||||
state._strong_obj = None
|
||||
|
||||
|
||||
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
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, state, key):
|
||||
self.state = state
|
||||
self.key = key
|
||||
|
||||
@property
|
||||
def loaded_value(self):
|
||||
"""The current value of this attribute as loaded from the database.
|
||||
|
||||
If the value has not been loaded, or is otherwise not present
|
||||
in the object's dictionary, returns NO_VALUE.
|
||||
|
||||
"""
|
||||
return self.state.dict.get(self.key, NO_VALUE)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""Return the value of this attribute.
|
||||
|
||||
This operation is equivalent to accessing the object's
|
||||
attribute directly or via ``getattr()``, and will fire
|
||||
off any pending loader callables if needed.
|
||||
|
||||
"""
|
||||
return self.state.manager[self.key].__get__(
|
||||
self.state.obj(), self.state.class_)
|
||||
|
||||
@property
|
||||
def history(self):
|
||||
"""Return the current pre-flush change history for
|
||||
this attribute, via the :class:`.History` interface.
|
||||
|
||||
"""
|
||||
return self.state.get_history(self.key,
|
||||
PASSIVE_NO_INITIALIZE)
|
||||
|
||||
|
||||
class PendingCollection(object):
|
||||
"""A writable placeholder for an unloaded collection.
|
||||
|
||||
Stores items appended to and removed from a collection that has not yet
|
||||
been loaded. When the collection is loaded, the changes stored in
|
||||
PendingCollection are applied to it to produce the final result.
|
||||
|
||||
"""
|
||||
def __init__(self):
|
||||
self.deleted_items = util.IdentitySet()
|
||||
self.added_items = util.OrderedIdentitySet()
|
||||
|
||||
def append(self, value):
|
||||
if value in self.deleted_items:
|
||||
self.deleted_items.remove(value)
|
||||
else:
|
||||
self.added_items.add(value)
|
||||
|
||||
def remove(self, value):
|
||||
if value in self.added_items:
|
||||
self.added_items.remove(value)
|
||||
else:
|
||||
self.deleted_items.add(value)
|
||||
Vendored
+1462
File diff suppressed because it is too large
Load Diff
Vendored
+118
@@ -0,0 +1,118 @@
|
||||
# orm/sync.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
|
||||
|
||||
"""private module containing functions used for copying data
|
||||
between instances based on join conditions.
|
||||
|
||||
"""
|
||||
|
||||
from . import exc, util as orm_util, attributes
|
||||
|
||||
|
||||
def populate(source, source_mapper, dest, dest_mapper,
|
||||
synchronize_pairs, uowcommit, flag_cascaded_pks):
|
||||
source_dict = source.dict
|
||||
dest_dict = dest.dict
|
||||
|
||||
for l, r in synchronize_pairs:
|
||||
try:
|
||||
# inline of source_mapper._get_state_attr_by_column
|
||||
prop = source_mapper._columntoproperty[l]
|
||||
value = source.manager[prop.key].impl.get(source, source_dict,
|
||||
attributes.PASSIVE_OFF)
|
||||
except exc.UnmappedColumnError:
|
||||
_raise_col_to_prop(False, source_mapper, l, dest_mapper, r)
|
||||
|
||||
try:
|
||||
# inline of dest_mapper._set_state_attr_by_column
|
||||
prop = dest_mapper._columntoproperty[r]
|
||||
dest.manager[prop.key].impl.set(dest, dest_dict, value, None)
|
||||
except exc.UnmappedColumnError:
|
||||
_raise_col_to_prop(True, source_mapper, l, dest_mapper, r)
|
||||
|
||||
# technically the "r.primary_key" check isn't
|
||||
# needed here, but we check for this condition to limit
|
||||
# how often this logic is invoked for memory/performance
|
||||
# reasons, since we only need this info for a primary key
|
||||
# destination.
|
||||
if flag_cascaded_pks and l.primary_key and \
|
||||
r.primary_key and \
|
||||
r.references(l):
|
||||
uowcommit.attributes[("pk_cascaded", dest, r)] = True
|
||||
|
||||
|
||||
def clear(dest, dest_mapper, synchronize_pairs):
|
||||
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, 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)
|
||||
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)
|
||||
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
|
||||
|
||||
"""
|
||||
for l, r in synchronize_pairs:
|
||||
try:
|
||||
prop = source_mapper._columntoproperty[l]
|
||||
except exc.UnmappedColumnError:
|
||||
_raise_col_to_prop(False, source_mapper, l, None, r)
|
||||
history = uowcommit.get_attribute_history(source, prop.key,
|
||||
attributes.PASSIVE_NO_INITIALIZE)
|
||||
if bool(history.deleted):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
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))
|
||||
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))
|
||||
Vendored
+648
@@ -0,0 +1,648 @@
|
||||
# orm/unitofwork.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
|
||||
|
||||
"""The internals for the unit of work system.
|
||||
|
||||
The session's flush() process passes objects to a contextual object
|
||||
here, which assembles flush tasks based on mappers and their properties,
|
||||
organizes them in order of dependency, and executes.
|
||||
|
||||
"""
|
||||
|
||||
from .. import util, event
|
||||
from ..util import topological
|
||||
from . import attributes, persistence, util as orm_util
|
||||
|
||||
sessionlib = util.importlater("sqlalchemy.orm", "session")
|
||||
|
||||
|
||||
def track_cascade_events(descriptor, prop):
|
||||
"""Establish event listeners on object attributes which handle
|
||||
cascade-on-set/append.
|
||||
|
||||
"""
|
||||
key = prop.key
|
||||
|
||||
def append(state, item, initiator):
|
||||
# process "save_update" cascade rules for when
|
||||
# an instance is appended to the list of another instance
|
||||
|
||||
if item is None:
|
||||
return
|
||||
|
||||
sess = sessionlib._state_session(state)
|
||||
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 \
|
||||
(prop.cascade_backrefs or key == initiator.key) and \
|
||||
not sess._contains_state(item_state):
|
||||
sess._save_or_update_state(item_state)
|
||||
return item
|
||||
|
||||
def remove(state, item, initiator):
|
||||
if item is None:
|
||||
return
|
||||
|
||||
sess = sessionlib._state_session(state)
|
||||
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 \
|
||||
item_state in sess._new and \
|
||||
prop.mapper._is_orphan(item_state):
|
||||
sess.expunge(item)
|
||||
|
||||
def set_(state, newvalue, oldvalue, initiator):
|
||||
# process "save_update" cascade rules for when an instance
|
||||
# is attached to another instance
|
||||
if oldvalue is newvalue:
|
||||
return newvalue
|
||||
|
||||
sess = sessionlib._state_session(state)
|
||||
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 \
|
||||
(prop.cascade_backrefs or key == initiator.key) and \
|
||||
not sess._contains_state(newvalue_state):
|
||||
sess._save_or_update_state(newvalue_state)
|
||||
|
||||
if oldvalue is not None and \
|
||||
oldvalue is not attributes.PASSIVE_NO_RESULT and \
|
||||
prop._cascade.delete_orphan:
|
||||
# possible to reach here with attributes.NEVER_SET ?
|
||||
oldvalue_state = attributes.instance_state(oldvalue)
|
||||
|
||||
if oldvalue_state in sess._new and \
|
||||
prop.mapper._is_orphan(oldvalue_state):
|
||||
sess.expunge(oldvalue)
|
||||
return newvalue
|
||||
|
||||
event.listen(descriptor, 'append', append, raw=True, retval=True)
|
||||
event.listen(descriptor, 'remove', remove, raw=True, retval=True)
|
||||
event.listen(descriptor, 'set', set_, raw=True, retval=True)
|
||||
|
||||
|
||||
class UOWTransaction(object):
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
|
||||
# dictionary used by external actors to
|
||||
# store arbitrary state information.
|
||||
self.attributes = {}
|
||||
|
||||
# dictionary of mappers to sets of
|
||||
# DependencyProcessors, which are also
|
||||
# set to be part of the sorted flush actions,
|
||||
# which have that mapper as a parent.
|
||||
self.deps = util.defaultdict(set)
|
||||
|
||||
# dictionary of mappers to sets of InstanceState
|
||||
# items pending for flush which have that mapper
|
||||
# as a parent.
|
||||
self.mappers = util.defaultdict(set)
|
||||
|
||||
# a dictionary of Preprocess objects, which gather
|
||||
# additional states impacted by the flush
|
||||
# and determine if a flush action is needed
|
||||
self.presort_actions = {}
|
||||
|
||||
# dictionary of PostSortRec objects, each
|
||||
# one issues work during the flush within
|
||||
# a certain ordering.
|
||||
self.postsort_actions = {}
|
||||
|
||||
# a set of 2-tuples, each containing two
|
||||
# PostSortRec objects where the second
|
||||
# is dependent on the first being executed
|
||||
# first
|
||||
self.dependencies = set()
|
||||
|
||||
# dictionary of InstanceState-> (isdelete, listonly)
|
||||
# tuples, indicating if this state is to be deleted
|
||||
# or insert/updated, or just refreshed
|
||||
self.states = {}
|
||||
|
||||
# tracks InstanceStates which will be receiving
|
||||
# a "post update" call. Keys are mappers,
|
||||
# values are a set of states and a set of the
|
||||
# columns which should be included in the update.
|
||||
self.post_update_states = util.defaultdict(lambda: (set(), set()))
|
||||
|
||||
@property
|
||||
def has_work(self):
|
||||
return bool(self.states)
|
||||
|
||||
def is_deleted(self, state):
|
||||
"""return true if the given state is marked as deleted
|
||||
within this uowtransaction."""
|
||||
|
||||
return state in self.states and self.states[state][0]
|
||||
|
||||
def memo(self, key, callable_):
|
||||
if key in self.attributes:
|
||||
return self.attributes[key]
|
||||
else:
|
||||
self.attributes[key] = ret = callable_()
|
||||
return ret
|
||||
|
||||
def remove_state_actions(self, state):
|
||||
"""remove pending actions for a state from the uowtransaction."""
|
||||
|
||||
isdelete = self.states[state][0]
|
||||
|
||||
self.states[state] = (isdelete, True)
|
||||
|
||||
def get_attribute_history(self, state, key,
|
||||
passive=attributes.PASSIVE_NO_INITIALIZE):
|
||||
"""facade to attributes.get_state_history(), including
|
||||
caching of results."""
|
||||
|
||||
hashkey = ("history", state, key)
|
||||
|
||||
# cache the objects, not the states; the strong reference here
|
||||
# prevents newly loaded objects from being dereferenced during the
|
||||
# flush process
|
||||
|
||||
if hashkey in self.attributes:
|
||||
history, 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 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.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)
|
||||
else:
|
||||
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 |
|
||||
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)
|
||||
|
||||
return state_history
|
||||
|
||||
def has_dep(self, processor):
|
||||
return (processor, True) in self.presort_actions
|
||||
|
||||
def register_preprocessor(self, processor, fromparent):
|
||||
key = (processor, fromparent)
|
||||
if key not in self.presort_actions:
|
||||
self.presort_actions[key] = Preprocess(processor, fromparent)
|
||||
|
||||
def register_object(self, state, isdelete=False,
|
||||
listonly=False, cancel_delete=False,
|
||||
operation=None, prop=None):
|
||||
if not self.session._contains_state(state):
|
||||
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" %
|
||||
(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:
|
||||
self._per_mapper_flush_actions(mapper)
|
||||
|
||||
self.mappers[mapper].add(state)
|
||||
self.states[state] = (isdelete, listonly)
|
||||
else:
|
||||
if not listonly and (isdelete or cancel_delete):
|
||||
self.states[state] = (isdelete, False)
|
||||
return True
|
||||
|
||||
def issue_post_update(self, state, post_update_cols):
|
||||
mapper = state.manager.mapper.base_mapper
|
||||
states, cols = self.post_update_states[mapper]
|
||||
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
|
||||
True or False, indicating if the DependencyProcessor operates
|
||||
on objects of that Mapper.
|
||||
|
||||
The result is stored in the dictionary persistently once
|
||||
calculated.
|
||||
|
||||
"""
|
||||
return util.PopulateDict(
|
||||
lambda tup: tup[0]._props.get(tup[1].key) is tup[1].prop
|
||||
)
|
||||
|
||||
def filter_states_for_dep(self, dep, states):
|
||||
"""Filter the given list of InstanceStates to those relevant to the
|
||||
given DependencyProcessor.
|
||||
|
||||
"""
|
||||
mapper_for_dep = self._mapper_for_dep
|
||||
return [s for s in states if mapper_for_dep[(s.manager.mapper, dep)]]
|
||||
|
||||
def states_for_mapper_hierarchy(self, mapper, isdelete, listonly):
|
||||
checktup = (isdelete, listonly)
|
||||
for mapper in mapper.base_mapper.self_and_descendants:
|
||||
for state in self.mappers[mapper]:
|
||||
if self.states[state] == checktup:
|
||||
yield state
|
||||
|
||||
def _generate_actions(self):
|
||||
"""Generate the full, unsorted collection of PostSortRecs as
|
||||
well as dependency pairs for this UOWTransaction.
|
||||
|
||||
"""
|
||||
# execute presort_actions, until all states
|
||||
# have been processed. a presort_action might
|
||||
# add new states to the uow.
|
||||
while True:
|
||||
ret = False
|
||||
for action in list(self.presort_actions.values()):
|
||||
if action.execute(self):
|
||||
ret = True
|
||||
if not ret:
|
||||
break
|
||||
|
||||
# see if the graph of mapper dependencies has cycles.
|
||||
self.cycles = cycles = topological.find_cycles(
|
||||
self.dependencies,
|
||||
self.postsort_actions.values())
|
||||
|
||||
if cycles:
|
||||
# if yes, break the per-mapper actions into
|
||||
# per-state actions
|
||||
convert = dict(
|
||||
(rec, set(rec.per_state_flush_actions(self)))
|
||||
for rec in cycles
|
||||
)
|
||||
|
||||
# rewrite the existing dependencies to point to
|
||||
# the per-state actions for those per-mapper actions
|
||||
# that were broken up.
|
||||
for edge in list(self.dependencies):
|
||||
if None in edge or \
|
||||
edge[0].disabled or edge[1].disabled or \
|
||||
cycles.issuperset(edge):
|
||||
self.dependencies.remove(edge)
|
||||
elif edge[0] in cycles:
|
||||
self.dependencies.remove(edge)
|
||||
for dep in convert[edge[0]]:
|
||||
self.dependencies.add((dep, edge[1]))
|
||||
elif edge[1] in cycles:
|
||||
self.dependencies.remove(edge)
|
||||
for dep in convert[edge[1]]:
|
||||
self.dependencies.add((edge[0], dep))
|
||||
|
||||
return set([a for a in self.postsort_actions.values()
|
||||
if not a.disabled
|
||||
]
|
||||
).difference(cycles)
|
||||
|
||||
def execute(self):
|
||||
postsort_actions = self._generate_actions()
|
||||
|
||||
#sort = topological.sort(self.dependencies, postsort_actions)
|
||||
#print "--------------"
|
||||
#print "\ndependencies:", self.dependencies
|
||||
#print "\ncycles:", self.cycles
|
||||
#print "\nsort:", list(sort)
|
||||
#print "\nCOUNT OF POSTSORT ACTIONS", len(postsort_actions)
|
||||
|
||||
# execute
|
||||
if self.cycles:
|
||||
for set_ in topological.sort_as_subsets(
|
||||
self.dependencies,
|
||||
postsort_actions):
|
||||
while set_:
|
||||
n = set_.pop()
|
||||
n.execute_aggregate(self, set_)
|
||||
else:
|
||||
for rec in topological.sort(
|
||||
self.dependencies,
|
||||
postsort_actions):
|
||||
rec.execute(self)
|
||||
|
||||
def finalize_flush_changes(self):
|
||||
"""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.
|
||||
|
||||
"""
|
||||
states = set(self.states)
|
||||
isdel = set(
|
||||
s for (s, (isdelete, listonly)) in self.states.iteritems()
|
||||
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
|
||||
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
|
||||
self.fromparent = fromparent
|
||||
self.processed = set()
|
||||
self.setup_flush_actions = False
|
||||
|
||||
def execute(self, uow):
|
||||
delete_states = set()
|
||||
save_states = set()
|
||||
|
||||
for mapper in self._mappers(uow):
|
||||
for state in uow.mappers[mapper].difference(self.processed):
|
||||
(isdelete, listonly) = uow.states[state]
|
||||
if not listonly:
|
||||
if isdelete:
|
||||
delete_states.add(state)
|
||||
else:
|
||||
save_states.add(state)
|
||||
|
||||
if delete_states:
|
||||
self.dependency_processor.presort_deletes(uow, delete_states)
|
||||
self.processed.update(delete_states)
|
||||
if save_states:
|
||||
self.dependency_processor.presort_saves(uow, save_states)
|
||||
self.processed.update(save_states)
|
||||
|
||||
if (delete_states or save_states):
|
||||
if not self.setup_flush_actions and (
|
||||
self.dependency_processor.\
|
||||
prop_has_changes(uow, delete_states, True) or
|
||||
self.dependency_processor.\
|
||||
prop_has_changes(uow, save_states, False)
|
||||
):
|
||||
self.dependency_processor.per_property_flush_actions(uow)
|
||||
self.setup_flush_actions = True
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class PostSortRec(object):
|
||||
disabled = False
|
||||
|
||||
def __new__(cls, uow, *args):
|
||||
key = (cls, ) + args
|
||||
if key in uow.postsort_actions:
|
||||
return uow.postsort_actions[key]
|
||||
else:
|
||||
uow.postsort_actions[key] = \
|
||||
ret = \
|
||||
object.__new__(cls)
|
||||
return ret
|
||||
|
||||
def execute_aggregate(self, uow, recs):
|
||||
self.execute(uow)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (
|
||||
self.__class__.__name__,
|
||||
",".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)
|
||||
|
||||
def execute(self, uow):
|
||||
states = self._elements(uow)
|
||||
if self.delete:
|
||||
self.dependency_processor.process_deletes(uow, states)
|
||||
else:
|
||||
self.dependency_processor.process_saves(uow, states)
|
||||
|
||||
def per_state_flush_actions(self, uow):
|
||||
# this is handled by SaveUpdateAll and DeleteAll,
|
||||
# since a ProcessAll should unconditionally be pulled
|
||||
# into per-state if either the parent/child mappers
|
||||
# are part of a cycle
|
||||
return iter([])
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s, delete=%s)" % (
|
||||
self.__class__.__name__,
|
||||
self.dependency_processor,
|
||||
self.delete
|
||||
)
|
||||
|
||||
def _elements(self, uow):
|
||||
for mapper in self._mappers(uow):
|
||||
for state in uow.mappers[mapper]:
|
||||
(isdelete, listonly) = uow.states[state]
|
||||
if isdelete == self.delete and not listonly:
|
||||
yield state
|
||||
|
||||
|
||||
class IssuePostUpdate(PostSortRec):
|
||||
def __init__(self, uow, mapper, isdelete):
|
||||
self.mapper = mapper
|
||||
self.isdelete = isdelete
|
||||
|
||||
def execute(self, uow):
|
||||
states, cols = uow.post_update_states[self.mapper]
|
||||
states = [s for s in states if uow.states[s][0] == self.isdelete]
|
||||
|
||||
persistence.post_update(self.mapper, states, uow, cols)
|
||||
|
||||
|
||||
class SaveUpdateAll(PostSortRec):
|
||||
def __init__(self, uow, mapper):
|
||||
self.mapper = mapper
|
||||
assert mapper is mapper.base_mapper
|
||||
|
||||
def execute(self, uow):
|
||||
persistence.save_obj(self.mapper,
|
||||
uow.states_for_mapper_hierarchy(self.mapper, False, False),
|
||||
uow
|
||||
)
|
||||
|
||||
def per_state_flush_actions(self, uow):
|
||||
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
|
||||
assert mapper is mapper.base_mapper
|
||||
|
||||
def execute(self, uow):
|
||||
persistence.delete_obj(self.mapper,
|
||||
uow.states_for_mapper_hierarchy(self.mapper, True, False),
|
||||
uow
|
||||
)
|
||||
|
||||
def per_state_flush_actions(self, uow):
|
||||
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
|
||||
self.delete = delete
|
||||
self.state = state
|
||||
|
||||
def execute_aggregate(self, uow, recs):
|
||||
cls_ = self.__class__
|
||||
dependency_processor = self.dependency_processor
|
||||
delete = self.delete
|
||||
our_recs = [r for r in recs
|
||||
if r.__class__ is cls_ and
|
||||
r.dependency_processor is dependency_processor and
|
||||
r.delete is delete]
|
||||
recs.difference_update(our_recs)
|
||||
states = [self.state] + [r.state for r in our_recs]
|
||||
if delete:
|
||||
dependency_processor.process_deletes(uow, states)
|
||||
else:
|
||||
dependency_processor.process_saves(uow, states)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s, %s, delete=%s)" % (
|
||||
self.__class__.__name__,
|
||||
self.dependency_processor,
|
||||
orm_util.state_str(self.state),
|
||||
self.delete
|
||||
)
|
||||
|
||||
|
||||
class SaveUpdateState(PostSortRec):
|
||||
def __init__(self, uow, state, mapper):
|
||||
self.state = state
|
||||
self.mapper = mapper
|
||||
|
||||
def execute_aggregate(self, uow, recs):
|
||||
cls_ = self.__class__
|
||||
mapper = self.mapper
|
||||
our_recs = [r for r in recs
|
||||
if r.__class__ is cls_ and
|
||||
r.mapper is mapper]
|
||||
recs.difference_update(our_recs)
|
||||
persistence.save_obj(mapper,
|
||||
[self.state] +
|
||||
[r.state for r in our_recs],
|
||||
uow)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (
|
||||
self.__class__.__name__,
|
||||
orm_util.state_str(self.state)
|
||||
)
|
||||
|
||||
|
||||
class DeleteState(PostSortRec):
|
||||
def __init__(self, uow, state, mapper):
|
||||
self.state = state
|
||||
self.mapper = mapper
|
||||
|
||||
def execute_aggregate(self, uow, recs):
|
||||
cls_ = self.__class__
|
||||
mapper = self.mapper
|
||||
our_recs = [r for r in recs
|
||||
if r.__class__ is cls_ and
|
||||
r.mapper is mapper]
|
||||
recs.difference_update(our_recs)
|
||||
states = [self.state] + [r.state for r in our_recs]
|
||||
persistence.delete_obj(mapper,
|
||||
[s for s in states if uow.states[s][0]],
|
||||
uow)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (
|
||||
self.__class__.__name__,
|
||||
orm_util.state_str(self.state)
|
||||
)
|
||||
Vendored
+1295
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user