You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							1707 lines
						
					
					
						
							53 KiB
						
					
					
				
			
		
		
	
	
							1707 lines
						
					
					
						
							53 KiB
						
					
					
				# orm/collections.py
 | 
						|
# Copyright (C) 2005-2022 the SQLAlchemy authors and contributors
 | 
						|
# <see AUTHORS file>
 | 
						|
#
 | 
						|
# This module is part of SQLAlchemy and is released under
 | 
						|
# the MIT License: https://www.opensource.org/licenses/mit-license.php
 | 
						|
 | 
						|
"""Support for collections of mapped entities.
 | 
						|
 | 
						|
The collections package supplies the machinery used to inform the ORM of
 | 
						|
collection membership changes.  An instrumentation via decoration approach is
 | 
						|
used, allowing arbitrary types (including built-ins) to be used as entity
 | 
						|
collections without requiring inheritance from a base class.
 | 
						|
 | 
						|
Instrumentation decoration relays membership change events to the
 | 
						|
:class:`.CollectionAttributeImpl` that is currently managing the collection.
 | 
						|
The decorators observe function call arguments and return values, tracking
 | 
						|
entities entering or leaving the collection.  Two decorator approaches are
 | 
						|
provided.  One is a bundle of generic decorators that map function arguments
 | 
						|
and return values to events::
 | 
						|
 | 
						|
  from sqlalchemy.orm.collections import collection
 | 
						|
  class MyClass(object):
 | 
						|
      # ...
 | 
						|
 | 
						|
      @collection.adds(1)
 | 
						|
      def store(self, item):
 | 
						|
          self.data.append(item)
 | 
						|
 | 
						|
      @collection.removes_return()
 | 
						|
      def pop(self):
 | 
						|
          return self.data.pop()
 | 
						|
 | 
						|
 | 
						|
The second approach is a bundle of targeted decorators that wrap appropriate
 | 
						|
append and remove notifiers around the mutation methods present in the
 | 
						|
standard Python ``list``, ``set`` and ``dict`` interfaces.  These could be
 | 
						|
specified in terms of generic decorator recipes, but are instead hand-tooled
 | 
						|
for increased efficiency.  The targeted decorators occasionally implement
 | 
						|
adapter-like behavior, such as mapping bulk-set methods (``extend``,
 | 
						|
``update``, ``__setslice__``, etc.) into the series of atomic mutation events
 | 
						|
that the ORM requires.
 | 
						|
 | 
						|
The targeted decorators are used internally for automatic instrumentation of
 | 
						|
entity collection classes.  Every collection class goes through a
 | 
						|
transformation process roughly like so:
 | 
						|
 | 
						|
1. If the class is a built-in, substitute a trivial sub-class
 | 
						|
2. Is this class already instrumented?
 | 
						|
3. Add in generic decorators
 | 
						|
4. Sniff out the collection interface through duck-typing
 | 
						|
5. Add targeted decoration to any undecorated interface method
 | 
						|
 | 
						|
This process modifies the class at runtime, decorating methods and adding some
 | 
						|
bookkeeping properties.  This isn't possible (or desirable) for built-in
 | 
						|
classes like ``list``, so trivial sub-classes are substituted to hold
 | 
						|
decoration::
 | 
						|
 | 
						|
  class InstrumentedList(list):
 | 
						|
      pass
 | 
						|
 | 
						|
Collection classes can be specified in ``relationship(collection_class=)`` as
 | 
						|
types or a function that returns an instance.  Collection classes are
 | 
						|
inspected and instrumented during the mapper compilation phase.  The
 | 
						|
collection_class callable will be executed once to produce a specimen
 | 
						|
instance, and the type of that specimen will be instrumented.  Functions that
 | 
						|
return built-in types like ``lists`` will be adapted to produce instrumented
 | 
						|
instances.
 | 
						|
 | 
						|
When extending a known type like ``list``, additional decorations are not
 | 
						|
generally not needed.  Odds are, the extension method will delegate to a
 | 
						|
method that's already instrumented.  For example::
 | 
						|
 | 
						|
  class QueueIsh(list):
 | 
						|
     def push(self, item):
 | 
						|
         self.append(item)
 | 
						|
     def shift(self):
 | 
						|
         return self.pop(0)
 | 
						|
 | 
						|
There's no need to decorate these methods.  ``append`` and ``pop`` are already
 | 
						|
instrumented as part of the ``list`` interface.  Decorating them would fire
 | 
						|
duplicate events, which should be avoided.
 | 
						|
 | 
						|
The targeted decoration tries not to rely on other methods in the underlying
 | 
						|
collection class, but some are unavoidable.  Many depend on 'read' methods
 | 
						|
being present to properly instrument a 'write', for example, ``__setitem__``
 | 
						|
needs ``__getitem__``.  "Bulk" methods like ``update`` and ``extend`` may also
 | 
						|
reimplemented in terms of atomic appends and removes, so the ``extend``
 | 
						|
decoration will actually perform many ``append`` operations and not call the
 | 
						|
underlying method at all.
 | 
						|
 | 
						|
Tight control over bulk operation and the firing of events is also possible by
 | 
						|
implementing the instrumentation internally in your methods.  The basic
 | 
						|
instrumentation package works under the general assumption that collection
 | 
						|
mutation will not raise unusual exceptions.  If you want to closely
 | 
						|
orchestrate append and remove events with exception management, internal
 | 
						|
instrumentation may be the answer.  Within your method,
 | 
						|
``collection_adapter(self)`` will retrieve an object that you can use for
 | 
						|
explicit control over triggering append and remove events.
 | 
						|
 | 
						|
The owning object and :class:`.CollectionAttributeImpl` are also reachable
 | 
						|
through the adapter, allowing for some very sophisticated behavior.
 | 
						|
 | 
						|
"""
 | 
						|
 | 
						|
import operator
 | 
						|
import weakref
 | 
						|
 | 
						|
from sqlalchemy.util.compat import inspect_getfullargspec
 | 
						|
from . import base
 | 
						|
from .. import exc as sa_exc
 | 
						|
from .. import util
 | 
						|
from ..sql import coercions
 | 
						|
from ..sql import expression
 | 
						|
from ..sql import roles
 | 
						|
 | 
						|
__all__ = [
 | 
						|
    "collection",
 | 
						|
    "collection_adapter",
 | 
						|
    "mapped_collection",
 | 
						|
    "column_mapped_collection",
 | 
						|
    "attribute_mapped_collection",
 | 
						|
]
 | 
						|
 | 
						|
__instrumentation_mutex = util.threading.Lock()
 | 
						|
 | 
						|
 | 
						|
class _PlainColumnGetter(object):
 | 
						|
    """Plain column getter, stores collection of Column objects
 | 
						|
    directly.
 | 
						|
 | 
						|
    Serializes to a :class:`._SerializableColumnGetterV2`
 | 
						|
    which has more expensive __call__() performance
 | 
						|
    and some rare caveats.
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, cols):
 | 
						|
        self.cols = cols
 | 
						|
        self.composite = len(cols) > 1
 | 
						|
 | 
						|
    def __reduce__(self):
 | 
						|
        return _SerializableColumnGetterV2._reduce_from_cols(self.cols)
 | 
						|
 | 
						|
    def _cols(self, mapper):
 | 
						|
        return self.cols
 | 
						|
 | 
						|
    def __call__(self, value):
 | 
						|
        state = base.instance_state(value)
 | 
						|
        m = base._state_mapper(state)
 | 
						|
 | 
						|
        key = [
 | 
						|
            m._get_state_attr_by_column(state, state.dict, col)
 | 
						|
            for col in self._cols(m)
 | 
						|
        ]
 | 
						|
 | 
						|
        if self.composite:
 | 
						|
            return tuple(key)
 | 
						|
        else:
 | 
						|
            return key[0]
 | 
						|
 | 
						|
 | 
						|
class _SerializableColumnGetter(object):
 | 
						|
    """Column-based getter used in version 0.7.6 only.
 | 
						|
 | 
						|
    Remains here for pickle compatibility with 0.7.6.
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, colkeys):
 | 
						|
        self.colkeys = colkeys
 | 
						|
        self.composite = len(colkeys) > 1
 | 
						|
 | 
						|
    def __reduce__(self):
 | 
						|
        return _SerializableColumnGetter, (self.colkeys,)
 | 
						|
 | 
						|
    def __call__(self, value):
 | 
						|
        state = base.instance_state(value)
 | 
						|
        m = base._state_mapper(state)
 | 
						|
        key = [
 | 
						|
            m._get_state_attr_by_column(
 | 
						|
                state, state.dict, m.mapped_table.columns[k]
 | 
						|
            )
 | 
						|
            for k in self.colkeys
 | 
						|
        ]
 | 
						|
        if self.composite:
 | 
						|
            return tuple(key)
 | 
						|
        else:
 | 
						|
            return key[0]
 | 
						|
 | 
						|
 | 
						|
class _SerializableColumnGetterV2(_PlainColumnGetter):
 | 
						|
    """Updated serializable getter which deals with
 | 
						|
    multi-table mapped classes.
 | 
						|
 | 
						|
    Two extremely unusual cases are not supported.
 | 
						|
    Mappings which have tables across multiple metadata
 | 
						|
    objects, or which are mapped to non-Table selectables
 | 
						|
    linked across inheriting mappers may fail to function
 | 
						|
    here.
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, colkeys):
 | 
						|
        self.colkeys = colkeys
 | 
						|
        self.composite = len(colkeys) > 1
 | 
						|
 | 
						|
    def __reduce__(self):
 | 
						|
        return self.__class__, (self.colkeys,)
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def _reduce_from_cols(cls, cols):
 | 
						|
        def _table_key(c):
 | 
						|
            if not isinstance(c.table, expression.TableClause):
 | 
						|
                return None
 | 
						|
            else:
 | 
						|
                return c.table.key
 | 
						|
 | 
						|
        colkeys = [(c.key, _table_key(c)) for c in cols]
 | 
						|
        return _SerializableColumnGetterV2, (colkeys,)
 | 
						|
 | 
						|
    def _cols(self, mapper):
 | 
						|
        cols = []
 | 
						|
        metadata = getattr(mapper.local_table, "metadata", None)
 | 
						|
        for (ckey, tkey) in self.colkeys:
 | 
						|
            if tkey is None or metadata is None or tkey not in metadata:
 | 
						|
                cols.append(mapper.local_table.c[ckey])
 | 
						|
            else:
 | 
						|
                cols.append(metadata.tables[tkey].c[ckey])
 | 
						|
        return cols
 | 
						|
 | 
						|
 | 
						|
def column_mapped_collection(mapping_spec):
 | 
						|
    """A dictionary-based collection type with column-based keying.
 | 
						|
 | 
						|
    Returns a :class:`.MappedCollection` factory with a keying function
 | 
						|
    generated from mapping_spec, which may be a Column or a sequence
 | 
						|
    of Columns.
 | 
						|
 | 
						|
    The key value must be immutable for the lifetime of the object.  You
 | 
						|
    can not, for example, map on foreign key values if those key values will
 | 
						|
    change during the session, i.e. from None to a database-assigned integer
 | 
						|
    after a session flush.
 | 
						|
 | 
						|
    """
 | 
						|
    cols = [
 | 
						|
        coercions.expect(roles.ColumnArgumentRole, q, argname="mapping_spec")
 | 
						|
        for q in util.to_list(mapping_spec)
 | 
						|
    ]
 | 
						|
    keyfunc = _PlainColumnGetter(cols)
 | 
						|
    return lambda: MappedCollection(keyfunc)
 | 
						|
 | 
						|
 | 
						|
class _SerializableAttrGetter(object):
 | 
						|
    def __init__(self, name):
 | 
						|
        self.name = name
 | 
						|
        self.getter = operator.attrgetter(name)
 | 
						|
 | 
						|
    def __call__(self, target):
 | 
						|
        return self.getter(target)
 | 
						|
 | 
						|
    def __reduce__(self):
 | 
						|
        return _SerializableAttrGetter, (self.name,)
 | 
						|
 | 
						|
 | 
						|
def attribute_mapped_collection(attr_name):
 | 
						|
    """A dictionary-based collection type with attribute-based keying.
 | 
						|
 | 
						|
    Returns a :class:`.MappedCollection` factory with a keying based on the
 | 
						|
    'attr_name' attribute of entities in the collection, where ``attr_name``
 | 
						|
    is the string name of the attribute.
 | 
						|
 | 
						|
    .. warning:: the key value must be assigned to its final value
 | 
						|
       **before** it is accessed by the attribute mapped collection.
 | 
						|
       Additionally, changes to the key attribute are **not tracked**
 | 
						|
       automatically, which means the key in the dictionary is not
 | 
						|
       automatically synchronized with the key value on the target object
 | 
						|
       itself.  See the section :ref:`key_collections_mutations`
 | 
						|
       for an example.
 | 
						|
 | 
						|
    """
 | 
						|
    getter = _SerializableAttrGetter(attr_name)
 | 
						|
    return lambda: MappedCollection(getter)
 | 
						|
 | 
						|
 | 
						|
def mapped_collection(keyfunc):
 | 
						|
    """A dictionary-based collection type with arbitrary keying.
 | 
						|
 | 
						|
    Returns a :class:`.MappedCollection` factory with a keying function
 | 
						|
    generated from keyfunc, a callable that takes an entity and returns a
 | 
						|
    key value.
 | 
						|
 | 
						|
    The key value must be immutable for the lifetime of the object.  You
 | 
						|
    can not, for example, map on foreign key values if those key values will
 | 
						|
    change during the session, i.e. from None to a database-assigned integer
 | 
						|
    after a session flush.
 | 
						|
 | 
						|
    """
 | 
						|
    return lambda: MappedCollection(keyfunc)
 | 
						|
 | 
						|
 | 
						|
class collection(object):
 | 
						|
    """Decorators for entity collection classes.
 | 
						|
 | 
						|
    The decorators fall into two groups: annotations and interception recipes.
 | 
						|
 | 
						|
    The annotating decorators (appender, remover, iterator, converter,
 | 
						|
    internally_instrumented) indicate the method's purpose and take no
 | 
						|
    arguments.  They are not written with parens::
 | 
						|
 | 
						|
        @collection.appender
 | 
						|
        def append(self, append): ...
 | 
						|
 | 
						|
    The recipe decorators all require parens, even those that take no
 | 
						|
    arguments::
 | 
						|
 | 
						|
        @collection.adds('entity')
 | 
						|
        def insert(self, position, entity): ...
 | 
						|
 | 
						|
        @collection.removes_return()
 | 
						|
        def popitem(self): ...
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    # Bundled as a class solely for ease of use: packaging, doc strings,
 | 
						|
    # importability.
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def appender(fn):
 | 
						|
        """Tag the method as the collection appender.
 | 
						|
 | 
						|
        The appender method is called with one positional argument: the value
 | 
						|
        to append. The method will be automatically decorated with 'adds(1)'
 | 
						|
        if not already decorated::
 | 
						|
 | 
						|
            @collection.appender
 | 
						|
            def add(self, append): ...
 | 
						|
 | 
						|
            # or, equivalently
 | 
						|
            @collection.appender
 | 
						|
            @collection.adds(1)
 | 
						|
            def add(self, append): ...
 | 
						|
 | 
						|
            # for mapping type, an 'append' may kick out a previous value
 | 
						|
            # that occupies that slot.  consider d['a'] = 'foo'- any previous
 | 
						|
            # value in d['a'] is discarded.
 | 
						|
            @collection.appender
 | 
						|
            @collection.replaces(1)
 | 
						|
            def add(self, entity):
 | 
						|
                key = some_key_func(entity)
 | 
						|
                previous = None
 | 
						|
                if key in self:
 | 
						|
                    previous = self[key]
 | 
						|
                self[key] = entity
 | 
						|
                return previous
 | 
						|
 | 
						|
        If the value to append is not allowed in the collection, you may
 | 
						|
        raise an exception.  Something to remember is that the appender
 | 
						|
        will be called for each object mapped by a database query.  If the
 | 
						|
        database contains rows that violate your collection semantics, you
 | 
						|
        will need to get creative to fix the problem, as access via the
 | 
						|
        collection will not work.
 | 
						|
 | 
						|
        If the appender method is internally instrumented, you must also
 | 
						|
        receive the keyword argument '_sa_initiator' and ensure its
 | 
						|
        promulgation to collection events.
 | 
						|
 | 
						|
        """
 | 
						|
        fn._sa_instrument_role = "appender"
 | 
						|
        return fn
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def remover(fn):
 | 
						|
        """Tag the method as the collection remover.
 | 
						|
 | 
						|
        The remover method is called with one positional argument: the value
 | 
						|
        to remove. The method will be automatically decorated with
 | 
						|
        :meth:`removes_return` if not already decorated::
 | 
						|
 | 
						|
            @collection.remover
 | 
						|
            def zap(self, entity): ...
 | 
						|
 | 
						|
            # or, equivalently
 | 
						|
            @collection.remover
 | 
						|
            @collection.removes_return()
 | 
						|
            def zap(self, ): ...
 | 
						|
 | 
						|
        If the value to remove is not present in the collection, you may
 | 
						|
        raise an exception or return None to ignore the error.
 | 
						|
 | 
						|
        If the remove method is internally instrumented, you must also
 | 
						|
        receive the keyword argument '_sa_initiator' and ensure its
 | 
						|
        promulgation to collection events.
 | 
						|
 | 
						|
        """
 | 
						|
        fn._sa_instrument_role = "remover"
 | 
						|
        return fn
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def iterator(fn):
 | 
						|
        """Tag the method as the collection remover.
 | 
						|
 | 
						|
        The iterator method is called with no arguments.  It is expected to
 | 
						|
        return an iterator over all collection members::
 | 
						|
 | 
						|
            @collection.iterator
 | 
						|
            def __iter__(self): ...
 | 
						|
 | 
						|
        """
 | 
						|
        fn._sa_instrument_role = "iterator"
 | 
						|
        return fn
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def internally_instrumented(fn):
 | 
						|
        """Tag the method as instrumented.
 | 
						|
 | 
						|
        This tag will prevent any decoration from being applied to the
 | 
						|
        method. Use this if you are orchestrating your own calls to
 | 
						|
        :func:`.collection_adapter` in one of the basic SQLAlchemy
 | 
						|
        interface methods, or to prevent an automatic ABC method
 | 
						|
        decoration from wrapping your implementation::
 | 
						|
 | 
						|
            # normally an 'extend' method on a list-like class would be
 | 
						|
            # automatically intercepted and re-implemented in terms of
 | 
						|
            # SQLAlchemy events and append().  your implementation will
 | 
						|
            # never be called, unless:
 | 
						|
            @collection.internally_instrumented
 | 
						|
            def extend(self, items): ...
 | 
						|
 | 
						|
        """
 | 
						|
        fn._sa_instrumented = True
 | 
						|
        return fn
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    @util.deprecated(
 | 
						|
        "1.3",
 | 
						|
        "The :meth:`.collection.converter` handler is deprecated and will "
 | 
						|
        "be removed in a future release.  Please refer to the "
 | 
						|
        ":class:`.AttributeEvents.bulk_replace` listener interface in "
 | 
						|
        "conjunction with the :func:`.event.listen` function.",
 | 
						|
    )
 | 
						|
    def converter(fn):
 | 
						|
        """Tag the method as the collection converter.
 | 
						|
 | 
						|
        This optional method will be called when a collection is being
 | 
						|
        replaced entirely, as in::
 | 
						|
 | 
						|
            myobj.acollection = [newvalue1, newvalue2]
 | 
						|
 | 
						|
        The converter method will receive the object being assigned and should
 | 
						|
        return an iterable of values suitable for use by the ``appender``
 | 
						|
        method.  A converter must not assign values or mutate the collection,
 | 
						|
        its sole job is to adapt the value the user provides into an iterable
 | 
						|
        of values for the ORM's use.
 | 
						|
 | 
						|
        The default converter implementation will use duck-typing to do the
 | 
						|
        conversion.  A dict-like collection will be convert into an iterable
 | 
						|
        of dictionary values, and other types will simply be iterated::
 | 
						|
 | 
						|
            @collection.converter
 | 
						|
            def convert(self, other): ...
 | 
						|
 | 
						|
        If the duck-typing of the object does not match the type of this
 | 
						|
        collection, a TypeError is raised.
 | 
						|
 | 
						|
        Supply an implementation of this method if you want to expand the
 | 
						|
        range of possible types that can be assigned in bulk or perform
 | 
						|
        validation on the values about to be assigned.
 | 
						|
 | 
						|
        """
 | 
						|
        fn._sa_instrument_role = "converter"
 | 
						|
        return fn
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def adds(arg):
 | 
						|
        """Mark the method as adding an entity to the collection.
 | 
						|
 | 
						|
        Adds "add to collection" handling to the method.  The decorator
 | 
						|
        argument indicates which method argument holds the SQLAlchemy-relevant
 | 
						|
        value.  Arguments can be specified positionally (i.e. integer) or by
 | 
						|
        name::
 | 
						|
 | 
						|
            @collection.adds(1)
 | 
						|
            def push(self, item): ...
 | 
						|
 | 
						|
            @collection.adds('entity')
 | 
						|
            def do_stuff(self, thing, entity=None): ...
 | 
						|
 | 
						|
        """
 | 
						|
 | 
						|
        def decorator(fn):
 | 
						|
            fn._sa_instrument_before = ("fire_append_event", arg)
 | 
						|
            return fn
 | 
						|
 | 
						|
        return decorator
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def replaces(arg):
 | 
						|
        """Mark the method as replacing an entity in the collection.
 | 
						|
 | 
						|
        Adds "add to collection" and "remove from collection" handling to
 | 
						|
        the method.  The decorator argument indicates which method argument
 | 
						|
        holds the SQLAlchemy-relevant value to be added, and return value, if
 | 
						|
        any will be considered the value to remove.
 | 
						|
 | 
						|
        Arguments can be specified positionally (i.e. integer) or by name::
 | 
						|
 | 
						|
            @collection.replaces(2)
 | 
						|
            def __setitem__(self, index, item): ...
 | 
						|
 | 
						|
        """
 | 
						|
 | 
						|
        def decorator(fn):
 | 
						|
            fn._sa_instrument_before = ("fire_append_event", arg)
 | 
						|
            fn._sa_instrument_after = "fire_remove_event"
 | 
						|
            return fn
 | 
						|
 | 
						|
        return decorator
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def removes(arg):
 | 
						|
        """Mark the method as removing an entity in the collection.
 | 
						|
 | 
						|
        Adds "remove from collection" handling to the method.  The decorator
 | 
						|
        argument indicates which method argument holds the SQLAlchemy-relevant
 | 
						|
        value to be removed. Arguments can be specified positionally (i.e.
 | 
						|
        integer) or by name::
 | 
						|
 | 
						|
            @collection.removes(1)
 | 
						|
            def zap(self, item): ...
 | 
						|
 | 
						|
        For methods where the value to remove is not known at call-time, use
 | 
						|
        collection.removes_return.
 | 
						|
 | 
						|
        """
 | 
						|
 | 
						|
        def decorator(fn):
 | 
						|
            fn._sa_instrument_before = ("fire_remove_event", arg)
 | 
						|
            return fn
 | 
						|
 | 
						|
        return decorator
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def removes_return():
 | 
						|
        """Mark the method as removing an entity in the collection.
 | 
						|
 | 
						|
        Adds "remove from collection" handling to the method.  The return
 | 
						|
        value of the method, if any, is considered the value to remove.  The
 | 
						|
        method arguments are not inspected::
 | 
						|
 | 
						|
            @collection.removes_return()
 | 
						|
            def pop(self): ...
 | 
						|
 | 
						|
        For methods where the value to remove is known at call-time, use
 | 
						|
        collection.remove.
 | 
						|
 | 
						|
        """
 | 
						|
 | 
						|
        def decorator(fn):
 | 
						|
            fn._sa_instrument_after = "fire_remove_event"
 | 
						|
            return fn
 | 
						|
 | 
						|
        return decorator
 | 
						|
 | 
						|
 | 
						|
collection_adapter = operator.attrgetter("_sa_adapter")
 | 
						|
"""Fetch the :class:`.CollectionAdapter` for a collection."""
 | 
						|
 | 
						|
 | 
						|
class CollectionAdapter(object):
 | 
						|
    """Bridges between the ORM and arbitrary Python collections.
 | 
						|
 | 
						|
    Proxies base-level collection operations (append, remove, iterate)
 | 
						|
    to the underlying Python collection, and emits add/remove events for
 | 
						|
    entities entering or leaving the collection.
 | 
						|
 | 
						|
    The ORM uses :class:`.CollectionAdapter` exclusively for interaction with
 | 
						|
    entity collections.
 | 
						|
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    __slots__ = (
 | 
						|
        "attr",
 | 
						|
        "_key",
 | 
						|
        "_data",
 | 
						|
        "owner_state",
 | 
						|
        "_converter",
 | 
						|
        "invalidated",
 | 
						|
        "empty",
 | 
						|
    )
 | 
						|
 | 
						|
    def __init__(self, attr, owner_state, data):
 | 
						|
        self.attr = attr
 | 
						|
        self._key = attr.key
 | 
						|
        self._data = weakref.ref(data)
 | 
						|
        self.owner_state = owner_state
 | 
						|
        data._sa_adapter = self
 | 
						|
        self._converter = data._sa_converter
 | 
						|
        self.invalidated = False
 | 
						|
        self.empty = False
 | 
						|
 | 
						|
    def _warn_invalidated(self):
 | 
						|
        util.warn("This collection has been invalidated.")
 | 
						|
 | 
						|
    @property
 | 
						|
    def data(self):
 | 
						|
        "The entity collection being adapted."
 | 
						|
        return self._data()
 | 
						|
 | 
						|
    @property
 | 
						|
    def _referenced_by_owner(self):
 | 
						|
        """return True if the owner state still refers to this collection.
 | 
						|
 | 
						|
        This will return False within a bulk replace operation,
 | 
						|
        where this collection is the one being replaced.
 | 
						|
 | 
						|
        """
 | 
						|
        return self.owner_state.dict[self._key] is self._data()
 | 
						|
 | 
						|
    def bulk_appender(self):
 | 
						|
        return self._data()._sa_appender
 | 
						|
 | 
						|
    def append_with_event(self, item, initiator=None):
 | 
						|
        """Add an entity to the collection, firing mutation events."""
 | 
						|
 | 
						|
        self._data()._sa_appender(item, _sa_initiator=initiator)
 | 
						|
 | 
						|
    def _set_empty(self, user_data):
 | 
						|
        assert (
 | 
						|
            not self.empty
 | 
						|
        ), "This collection adapter is already in the 'empty' state"
 | 
						|
        self.empty = True
 | 
						|
        self.owner_state._empty_collections[self._key] = user_data
 | 
						|
 | 
						|
    def _reset_empty(self):
 | 
						|
        assert (
 | 
						|
            self.empty
 | 
						|
        ), "This collection adapter is not in the 'empty' state"
 | 
						|
        self.empty = False
 | 
						|
        self.owner_state.dict[
 | 
						|
            self._key
 | 
						|
        ] = self.owner_state._empty_collections.pop(self._key)
 | 
						|
 | 
						|
    def _refuse_empty(self):
 | 
						|
        raise sa_exc.InvalidRequestError(
 | 
						|
            "This is a special 'empty' collection which cannot accommodate "
 | 
						|
            "internal mutation operations"
 | 
						|
        )
 | 
						|
 | 
						|
    def append_without_event(self, item):
 | 
						|
        """Add or restore an entity to the collection, firing no events."""
 | 
						|
 | 
						|
        if self.empty:
 | 
						|
            self._refuse_empty()
 | 
						|
        self._data()._sa_appender(item, _sa_initiator=False)
 | 
						|
 | 
						|
    def append_multiple_without_event(self, items):
 | 
						|
        """Add or restore an entity to the collection, firing no events."""
 | 
						|
        if self.empty:
 | 
						|
            self._refuse_empty()
 | 
						|
        appender = self._data()._sa_appender
 | 
						|
        for item in items:
 | 
						|
            appender(item, _sa_initiator=False)
 | 
						|
 | 
						|
    def bulk_remover(self):
 | 
						|
        return self._data()._sa_remover
 | 
						|
 | 
						|
    def remove_with_event(self, item, initiator=None):
 | 
						|
        """Remove an entity from the collection, firing mutation events."""
 | 
						|
        self._data()._sa_remover(item, _sa_initiator=initiator)
 | 
						|
 | 
						|
    def remove_without_event(self, item):
 | 
						|
        """Remove an entity from the collection, firing no events."""
 | 
						|
        if self.empty:
 | 
						|
            self._refuse_empty()
 | 
						|
        self._data()._sa_remover(item, _sa_initiator=False)
 | 
						|
 | 
						|
    def clear_with_event(self, initiator=None):
 | 
						|
        """Empty the collection, firing a mutation event for each entity."""
 | 
						|
 | 
						|
        if self.empty:
 | 
						|
            self._refuse_empty()
 | 
						|
        remover = self._data()._sa_remover
 | 
						|
        for item in list(self):
 | 
						|
            remover(item, _sa_initiator=initiator)
 | 
						|
 | 
						|
    def clear_without_event(self):
 | 
						|
        """Empty the collection, firing no events."""
 | 
						|
 | 
						|
        if self.empty:
 | 
						|
            self._refuse_empty()
 | 
						|
        remover = self._data()._sa_remover
 | 
						|
        for item in list(self):
 | 
						|
            remover(item, _sa_initiator=False)
 | 
						|
 | 
						|
    def __iter__(self):
 | 
						|
        """Iterate over entities in the collection."""
 | 
						|
 | 
						|
        return iter(self._data()._sa_iterator())
 | 
						|
 | 
						|
    def __len__(self):
 | 
						|
        """Count entities in the collection."""
 | 
						|
        return len(list(self._data()._sa_iterator()))
 | 
						|
 | 
						|
    def __bool__(self):
 | 
						|
        return True
 | 
						|
 | 
						|
    __nonzero__ = __bool__
 | 
						|
 | 
						|
    def fire_append_wo_mutation_event(self, item, initiator=None):
 | 
						|
        """Notify that a entity is entering the collection but is already
 | 
						|
        present.
 | 
						|
 | 
						|
 | 
						|
        Initiator is a token owned by the InstrumentedAttribute that
 | 
						|
        initiated the membership mutation, and should be left as None
 | 
						|
        unless you are passing along an initiator value from a chained
 | 
						|
        operation.
 | 
						|
 | 
						|
        .. versionadded:: 1.4.15
 | 
						|
 | 
						|
        """
 | 
						|
        if initiator is not False:
 | 
						|
            if self.invalidated:
 | 
						|
                self._warn_invalidated()
 | 
						|
 | 
						|
            if self.empty:
 | 
						|
                self._reset_empty()
 | 
						|
 | 
						|
            return self.attr.fire_append_wo_mutation_event(
 | 
						|
                self.owner_state, self.owner_state.dict, item, initiator
 | 
						|
            )
 | 
						|
        else:
 | 
						|
            return item
 | 
						|
 | 
						|
    def fire_append_event(self, item, initiator=None):
 | 
						|
        """Notify that a entity has entered the collection.
 | 
						|
 | 
						|
        Initiator is a token owned by the InstrumentedAttribute that
 | 
						|
        initiated the membership mutation, and should be left as None
 | 
						|
        unless you are passing along an initiator value from a chained
 | 
						|
        operation.
 | 
						|
 | 
						|
        """
 | 
						|
        if initiator is not False:
 | 
						|
            if self.invalidated:
 | 
						|
                self._warn_invalidated()
 | 
						|
 | 
						|
            if self.empty:
 | 
						|
                self._reset_empty()
 | 
						|
 | 
						|
            return self.attr.fire_append_event(
 | 
						|
                self.owner_state, self.owner_state.dict, item, initiator
 | 
						|
            )
 | 
						|
        else:
 | 
						|
            return item
 | 
						|
 | 
						|
    def fire_remove_event(self, item, initiator=None):
 | 
						|
        """Notify that a entity has been removed from the collection.
 | 
						|
 | 
						|
        Initiator is the InstrumentedAttribute that initiated the membership
 | 
						|
        mutation, and should be left as None unless you are passing along
 | 
						|
        an initiator value from a chained operation.
 | 
						|
 | 
						|
        """
 | 
						|
        if initiator is not False:
 | 
						|
            if self.invalidated:
 | 
						|
                self._warn_invalidated()
 | 
						|
 | 
						|
            if self.empty:
 | 
						|
                self._reset_empty()
 | 
						|
 | 
						|
            self.attr.fire_remove_event(
 | 
						|
                self.owner_state, self.owner_state.dict, item, initiator
 | 
						|
            )
 | 
						|
 | 
						|
    def fire_pre_remove_event(self, initiator=None):
 | 
						|
        """Notify that an entity is about to be removed from the collection.
 | 
						|
 | 
						|
        Only called if the entity cannot be removed after calling
 | 
						|
        fire_remove_event().
 | 
						|
 | 
						|
        """
 | 
						|
        if self.invalidated:
 | 
						|
            self._warn_invalidated()
 | 
						|
        self.attr.fire_pre_remove_event(
 | 
						|
            self.owner_state, self.owner_state.dict, initiator=initiator
 | 
						|
        )
 | 
						|
 | 
						|
    def __getstate__(self):
 | 
						|
        return {
 | 
						|
            "key": self._key,
 | 
						|
            "owner_state": self.owner_state,
 | 
						|
            "owner_cls": self.owner_state.class_,
 | 
						|
            "data": self.data,
 | 
						|
            "invalidated": self.invalidated,
 | 
						|
            "empty": self.empty,
 | 
						|
        }
 | 
						|
 | 
						|
    def __setstate__(self, d):
 | 
						|
        self._key = d["key"]
 | 
						|
        self.owner_state = d["owner_state"]
 | 
						|
        self._data = weakref.ref(d["data"])
 | 
						|
        self._converter = d["data"]._sa_converter
 | 
						|
        d["data"]._sa_adapter = self
 | 
						|
        self.invalidated = d["invalidated"]
 | 
						|
        self.attr = getattr(d["owner_cls"], self._key).impl
 | 
						|
        self.empty = d.get("empty", False)
 | 
						|
 | 
						|
 | 
						|
def bulk_replace(values, existing_adapter, new_adapter, initiator=None):
 | 
						|
    """Load a new collection, firing events based on prior like membership.
 | 
						|
 | 
						|
    Appends instances in ``values`` onto the ``new_adapter``. Events will be
 | 
						|
    fired for any instance not present in the ``existing_adapter``.  Any
 | 
						|
    instances in ``existing_adapter`` not present in ``values`` will have
 | 
						|
    remove events fired upon them.
 | 
						|
 | 
						|
    :param values: An iterable of collection member instances
 | 
						|
 | 
						|
    :param existing_adapter: A :class:`.CollectionAdapter` of
 | 
						|
     instances to be replaced
 | 
						|
 | 
						|
    :param new_adapter: An empty :class:`.CollectionAdapter`
 | 
						|
     to load with ``values``
 | 
						|
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    assert isinstance(values, list)
 | 
						|
 | 
						|
    idset = util.IdentitySet
 | 
						|
    existing_idset = idset(existing_adapter or ())
 | 
						|
    constants = existing_idset.intersection(values or ())
 | 
						|
    additions = idset(values or ()).difference(constants)
 | 
						|
    removals = existing_idset.difference(constants)
 | 
						|
 | 
						|
    appender = new_adapter.bulk_appender()
 | 
						|
 | 
						|
    for member in values or ():
 | 
						|
        if member in additions:
 | 
						|
            appender(member, _sa_initiator=initiator)
 | 
						|
        elif member in constants:
 | 
						|
            appender(member, _sa_initiator=False)
 | 
						|
 | 
						|
    if existing_adapter:
 | 
						|
        for member in removals:
 | 
						|
            existing_adapter.fire_remove_event(member, initiator=initiator)
 | 
						|
 | 
						|
 | 
						|
def prepare_instrumentation(factory):
 | 
						|
    """Prepare a callable for future use as a collection class factory.
 | 
						|
 | 
						|
    Given a collection class factory (either a type or no-arg callable),
 | 
						|
    return another factory that will produce compatible instances when
 | 
						|
    called.
 | 
						|
 | 
						|
    This function is responsible for converting collection_class=list
 | 
						|
    into the run-time behavior of collection_class=InstrumentedList.
 | 
						|
 | 
						|
    """
 | 
						|
    # Convert a builtin to 'Instrumented*'
 | 
						|
    if factory in __canned_instrumentation:
 | 
						|
        factory = __canned_instrumentation[factory]
 | 
						|
 | 
						|
    # Create a specimen
 | 
						|
    cls = type(factory())
 | 
						|
 | 
						|
    # Did factory callable return a builtin?
 | 
						|
    if cls in __canned_instrumentation:
 | 
						|
        # Wrap it so that it returns our 'Instrumented*'
 | 
						|
        factory = __converting_factory(cls, factory)
 | 
						|
        cls = factory()
 | 
						|
 | 
						|
    # Instrument the class if needed.
 | 
						|
    if __instrumentation_mutex.acquire():
 | 
						|
        try:
 | 
						|
            if getattr(cls, "_sa_instrumented", None) != id(cls):
 | 
						|
                _instrument_class(cls)
 | 
						|
        finally:
 | 
						|
            __instrumentation_mutex.release()
 | 
						|
 | 
						|
    return factory
 | 
						|
 | 
						|
 | 
						|
def __converting_factory(specimen_cls, original_factory):
 | 
						|
    """Return a wrapper that converts a "canned" collection like
 | 
						|
    set, dict, list into the Instrumented* version.
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    instrumented_cls = __canned_instrumentation[specimen_cls]
 | 
						|
 | 
						|
    def wrapper():
 | 
						|
        collection = original_factory()
 | 
						|
        return instrumented_cls(collection)
 | 
						|
 | 
						|
    # often flawed but better than nothing
 | 
						|
    wrapper.__name__ = "%sWrapper" % original_factory.__name__
 | 
						|
    wrapper.__doc__ = original_factory.__doc__
 | 
						|
 | 
						|
    return wrapper
 | 
						|
 | 
						|
 | 
						|
def _instrument_class(cls):
 | 
						|
    """Modify methods in a class and install instrumentation."""
 | 
						|
 | 
						|
    # In the normal call flow, a request for any of the 3 basic collection
 | 
						|
    # types is transformed into one of our trivial subclasses
 | 
						|
    # (e.g. InstrumentedList).  Catch anything else that sneaks in here...
 | 
						|
    if cls.__module__ == "__builtin__":
 | 
						|
        raise sa_exc.ArgumentError(
 | 
						|
            "Can not instrument a built-in type. Use a "
 | 
						|
            "subclass, even a trivial one."
 | 
						|
        )
 | 
						|
 | 
						|
    roles, methods = _locate_roles_and_methods(cls)
 | 
						|
 | 
						|
    _setup_canned_roles(cls, roles, methods)
 | 
						|
 | 
						|
    _assert_required_roles(cls, roles, methods)
 | 
						|
 | 
						|
    _set_collection_attributes(cls, roles, methods)
 | 
						|
 | 
						|
 | 
						|
def _locate_roles_and_methods(cls):
 | 
						|
    """search for _sa_instrument_role-decorated methods in
 | 
						|
    method resolution order, assign to roles.
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    roles = {}
 | 
						|
    methods = {}
 | 
						|
 | 
						|
    for supercls in cls.__mro__:
 | 
						|
        for name, method in vars(supercls).items():
 | 
						|
            if not callable(method):
 | 
						|
                continue
 | 
						|
 | 
						|
            # note role declarations
 | 
						|
            if hasattr(method, "_sa_instrument_role"):
 | 
						|
                role = method._sa_instrument_role
 | 
						|
                assert role in (
 | 
						|
                    "appender",
 | 
						|
                    "remover",
 | 
						|
                    "iterator",
 | 
						|
                    "converter",
 | 
						|
                )
 | 
						|
                roles.setdefault(role, name)
 | 
						|
 | 
						|
            # transfer instrumentation requests from decorated function
 | 
						|
            # to the combined queue
 | 
						|
            before, after = None, None
 | 
						|
            if hasattr(method, "_sa_instrument_before"):
 | 
						|
                op, argument = method._sa_instrument_before
 | 
						|
                assert op in ("fire_append_event", "fire_remove_event")
 | 
						|
                before = op, argument
 | 
						|
            if hasattr(method, "_sa_instrument_after"):
 | 
						|
                op = method._sa_instrument_after
 | 
						|
                assert op in ("fire_append_event", "fire_remove_event")
 | 
						|
                after = op
 | 
						|
            if before:
 | 
						|
                methods[name] = before + (after,)
 | 
						|
            elif after:
 | 
						|
                methods[name] = None, None, after
 | 
						|
    return roles, methods
 | 
						|
 | 
						|
 | 
						|
def _setup_canned_roles(cls, roles, methods):
 | 
						|
    """see if this class has "canned" roles based on a known
 | 
						|
    collection type (dict, set, list).  Apply those roles
 | 
						|
    as needed to the "roles" dictionary, and also
 | 
						|
    prepare "decorator" methods
 | 
						|
 | 
						|
    """
 | 
						|
    collection_type = util.duck_type_collection(cls)
 | 
						|
    if collection_type in __interfaces:
 | 
						|
        canned_roles, decorators = __interfaces[collection_type]
 | 
						|
        for role, name in canned_roles.items():
 | 
						|
            roles.setdefault(role, name)
 | 
						|
 | 
						|
        # apply ABC auto-decoration to methods that need it
 | 
						|
        for method, decorator in decorators.items():
 | 
						|
            fn = getattr(cls, method, None)
 | 
						|
            if (
 | 
						|
                fn
 | 
						|
                and method not in methods
 | 
						|
                and not hasattr(fn, "_sa_instrumented")
 | 
						|
            ):
 | 
						|
                setattr(cls, method, decorator(fn))
 | 
						|
 | 
						|
 | 
						|
def _assert_required_roles(cls, roles, methods):
 | 
						|
    """ensure all roles are present, and apply implicit instrumentation if
 | 
						|
    needed
 | 
						|
 | 
						|
    """
 | 
						|
    if "appender" not in roles or not hasattr(cls, roles["appender"]):
 | 
						|
        raise sa_exc.ArgumentError(
 | 
						|
            "Type %s must elect an appender method to be "
 | 
						|
            "a collection class" % cls.__name__
 | 
						|
        )
 | 
						|
    elif roles["appender"] not in methods and not hasattr(
 | 
						|
        getattr(cls, roles["appender"]), "_sa_instrumented"
 | 
						|
    ):
 | 
						|
        methods[roles["appender"]] = ("fire_append_event", 1, None)
 | 
						|
 | 
						|
    if "remover" not in roles or not hasattr(cls, roles["remover"]):
 | 
						|
        raise sa_exc.ArgumentError(
 | 
						|
            "Type %s must elect a remover method to be "
 | 
						|
            "a collection class" % cls.__name__
 | 
						|
        )
 | 
						|
    elif roles["remover"] not in methods and not hasattr(
 | 
						|
        getattr(cls, roles["remover"]), "_sa_instrumented"
 | 
						|
    ):
 | 
						|
        methods[roles["remover"]] = ("fire_remove_event", 1, None)
 | 
						|
 | 
						|
    if "iterator" not in roles or not hasattr(cls, roles["iterator"]):
 | 
						|
        raise sa_exc.ArgumentError(
 | 
						|
            "Type %s must elect an iterator method to be "
 | 
						|
            "a collection class" % cls.__name__
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
def _set_collection_attributes(cls, roles, methods):
 | 
						|
    """apply ad-hoc instrumentation from decorators, class-level defaults
 | 
						|
    and implicit role declarations
 | 
						|
 | 
						|
    """
 | 
						|
    for method_name, (before, argument, after) in methods.items():
 | 
						|
        setattr(
 | 
						|
            cls,
 | 
						|
            method_name,
 | 
						|
            _instrument_membership_mutator(
 | 
						|
                getattr(cls, method_name), before, argument, after
 | 
						|
            ),
 | 
						|
        )
 | 
						|
    # intern the role map
 | 
						|
    for role, method_name in roles.items():
 | 
						|
        setattr(cls, "_sa_%s" % role, getattr(cls, method_name))
 | 
						|
 | 
						|
    cls._sa_adapter = None
 | 
						|
 | 
						|
    if not hasattr(cls, "_sa_converter"):
 | 
						|
        cls._sa_converter = None
 | 
						|
    cls._sa_instrumented = id(cls)
 | 
						|
 | 
						|
 | 
						|
def _instrument_membership_mutator(method, before, argument, after):
 | 
						|
    """Route method args and/or return value through the collection
 | 
						|
    adapter."""
 | 
						|
    # This isn't smart enough to handle @adds(1) for 'def fn(self, (a, b))'
 | 
						|
    if before:
 | 
						|
        fn_args = list(
 | 
						|
            util.flatten_iterator(inspect_getfullargspec(method)[0])
 | 
						|
        )
 | 
						|
        if isinstance(argument, int):
 | 
						|
            pos_arg = argument
 | 
						|
            named_arg = len(fn_args) > argument and fn_args[argument] or None
 | 
						|
        else:
 | 
						|
            if argument in fn_args:
 | 
						|
                pos_arg = fn_args.index(argument)
 | 
						|
            else:
 | 
						|
                pos_arg = None
 | 
						|
            named_arg = argument
 | 
						|
        del fn_args
 | 
						|
 | 
						|
    def wrapper(*args, **kw):
 | 
						|
        if before:
 | 
						|
            if pos_arg is None:
 | 
						|
                if named_arg not in kw:
 | 
						|
                    raise sa_exc.ArgumentError(
 | 
						|
                        "Missing argument %s" % argument
 | 
						|
                    )
 | 
						|
                value = kw[named_arg]
 | 
						|
            else:
 | 
						|
                if len(args) > pos_arg:
 | 
						|
                    value = args[pos_arg]
 | 
						|
                elif named_arg in kw:
 | 
						|
                    value = kw[named_arg]
 | 
						|
                else:
 | 
						|
                    raise sa_exc.ArgumentError(
 | 
						|
                        "Missing argument %s" % argument
 | 
						|
                    )
 | 
						|
 | 
						|
        initiator = kw.pop("_sa_initiator", None)
 | 
						|
        if initiator is False:
 | 
						|
            executor = None
 | 
						|
        else:
 | 
						|
            executor = args[0]._sa_adapter
 | 
						|
 | 
						|
        if before and executor:
 | 
						|
            getattr(executor, before)(value, initiator)
 | 
						|
 | 
						|
        if not after or not executor:
 | 
						|
            return method(*args, **kw)
 | 
						|
        else:
 | 
						|
            res = method(*args, **kw)
 | 
						|
            if res is not None:
 | 
						|
                getattr(executor, after)(res, initiator)
 | 
						|
            return res
 | 
						|
 | 
						|
    wrapper._sa_instrumented = True
 | 
						|
    if hasattr(method, "_sa_instrument_role"):
 | 
						|
        wrapper._sa_instrument_role = method._sa_instrument_role
 | 
						|
    wrapper.__name__ = method.__name__
 | 
						|
    wrapper.__doc__ = method.__doc__
 | 
						|
    return wrapper
 | 
						|
 | 
						|
 | 
						|
def __set_wo_mutation(collection, item, _sa_initiator=None):
 | 
						|
    """Run set wo mutation events.
 | 
						|
 | 
						|
    The collection is not mutated.
 | 
						|
 | 
						|
    """
 | 
						|
    if _sa_initiator is not False:
 | 
						|
        executor = collection._sa_adapter
 | 
						|
        if executor:
 | 
						|
            executor.fire_append_wo_mutation_event(item, _sa_initiator)
 | 
						|
 | 
						|
 | 
						|
def __set(collection, item, _sa_initiator=None):
 | 
						|
    """Run set events.
 | 
						|
 | 
						|
    This event always occurs before the collection is actually mutated.
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    if _sa_initiator is not False:
 | 
						|
        executor = collection._sa_adapter
 | 
						|
        if executor:
 | 
						|
            item = executor.fire_append_event(item, _sa_initiator)
 | 
						|
    return item
 | 
						|
 | 
						|
 | 
						|
def __del(collection, item, _sa_initiator=None):
 | 
						|
    """Run del events.
 | 
						|
 | 
						|
    This event occurs before the collection is actually mutated, *except*
 | 
						|
    in the case of a pop operation, in which case it occurs afterwards.
 | 
						|
    For pop operations, the __before_pop hook is called before the
 | 
						|
    operation occurs.
 | 
						|
 | 
						|
    """
 | 
						|
    if _sa_initiator is not False:
 | 
						|
        executor = collection._sa_adapter
 | 
						|
        if executor:
 | 
						|
            executor.fire_remove_event(item, _sa_initiator)
 | 
						|
 | 
						|
 | 
						|
def __before_pop(collection, _sa_initiator=None):
 | 
						|
    """An event which occurs on a before a pop() operation occurs."""
 | 
						|
    executor = collection._sa_adapter
 | 
						|
    if executor:
 | 
						|
        executor.fire_pre_remove_event(_sa_initiator)
 | 
						|
 | 
						|
 | 
						|
def _list_decorators():
 | 
						|
    """Tailored instrumentation wrappers for any list-like class."""
 | 
						|
 | 
						|
    def _tidy(fn):
 | 
						|
        fn._sa_instrumented = True
 | 
						|
        fn.__doc__ = getattr(list, fn.__name__).__doc__
 | 
						|
 | 
						|
    def append(fn):
 | 
						|
        def append(self, item, _sa_initiator=None):
 | 
						|
            item = __set(self, item, _sa_initiator)
 | 
						|
            fn(self, item)
 | 
						|
 | 
						|
        _tidy(append)
 | 
						|
        return append
 | 
						|
 | 
						|
    def remove(fn):
 | 
						|
        def remove(self, value, _sa_initiator=None):
 | 
						|
            __del(self, value, _sa_initiator)
 | 
						|
            # testlib.pragma exempt:__eq__
 | 
						|
            fn(self, value)
 | 
						|
 | 
						|
        _tidy(remove)
 | 
						|
        return remove
 | 
						|
 | 
						|
    def insert(fn):
 | 
						|
        def insert(self, index, value):
 | 
						|
            value = __set(self, value)
 | 
						|
            fn(self, index, value)
 | 
						|
 | 
						|
        _tidy(insert)
 | 
						|
        return insert
 | 
						|
 | 
						|
    def __setitem__(fn):
 | 
						|
        def __setitem__(self, index, value):
 | 
						|
            if not isinstance(index, slice):
 | 
						|
                existing = self[index]
 | 
						|
                if existing is not None:
 | 
						|
                    __del(self, existing)
 | 
						|
                value = __set(self, value)
 | 
						|
                fn(self, index, value)
 | 
						|
            else:
 | 
						|
                # slice assignment requires __delitem__, insert, __len__
 | 
						|
                step = index.step or 1
 | 
						|
                start = index.start or 0
 | 
						|
                if start < 0:
 | 
						|
                    start += len(self)
 | 
						|
                if index.stop is not None:
 | 
						|
                    stop = index.stop
 | 
						|
                else:
 | 
						|
                    stop = len(self)
 | 
						|
                if stop < 0:
 | 
						|
                    stop += len(self)
 | 
						|
 | 
						|
                if step == 1:
 | 
						|
                    if value is self:
 | 
						|
                        return
 | 
						|
                    for i in range(start, stop, step):
 | 
						|
                        if len(self) > start:
 | 
						|
                            del self[start]
 | 
						|
 | 
						|
                    for i, item in enumerate(value):
 | 
						|
                        self.insert(i + start, item)
 | 
						|
                else:
 | 
						|
                    rng = list(range(start, stop, step))
 | 
						|
                    if len(value) != len(rng):
 | 
						|
                        raise ValueError(
 | 
						|
                            "attempt to assign sequence of size %s to "
 | 
						|
                            "extended slice of size %s"
 | 
						|
                            % (len(value), len(rng))
 | 
						|
                        )
 | 
						|
                    for i, item in zip(rng, value):
 | 
						|
                        self.__setitem__(i, item)
 | 
						|
 | 
						|
        _tidy(__setitem__)
 | 
						|
        return __setitem__
 | 
						|
 | 
						|
    def __delitem__(fn):
 | 
						|
        def __delitem__(self, index):
 | 
						|
            if not isinstance(index, slice):
 | 
						|
                item = self[index]
 | 
						|
                __del(self, item)
 | 
						|
                fn(self, index)
 | 
						|
            else:
 | 
						|
                # slice deletion requires __getslice__ and a slice-groking
 | 
						|
                # __getitem__ for stepped deletion
 | 
						|
                # note: not breaking this into atomic dels
 | 
						|
                for item in self[index]:
 | 
						|
                    __del(self, item)
 | 
						|
                fn(self, index)
 | 
						|
 | 
						|
        _tidy(__delitem__)
 | 
						|
        return __delitem__
 | 
						|
 | 
						|
    if util.py2k:
 | 
						|
 | 
						|
        def __setslice__(fn):
 | 
						|
            def __setslice__(self, start, end, values):
 | 
						|
                for value in self[start:end]:
 | 
						|
                    __del(self, value)
 | 
						|
                values = [__set(self, value) for value in values]
 | 
						|
                fn(self, start, end, values)
 | 
						|
 | 
						|
            _tidy(__setslice__)
 | 
						|
            return __setslice__
 | 
						|
 | 
						|
        def __delslice__(fn):
 | 
						|
            def __delslice__(self, start, end):
 | 
						|
                for value in self[start:end]:
 | 
						|
                    __del(self, value)
 | 
						|
                fn(self, start, end)
 | 
						|
 | 
						|
            _tidy(__delslice__)
 | 
						|
            return __delslice__
 | 
						|
 | 
						|
    def extend(fn):
 | 
						|
        def extend(self, iterable):
 | 
						|
            for value in list(iterable):
 | 
						|
                self.append(value)
 | 
						|
 | 
						|
        _tidy(extend)
 | 
						|
        return extend
 | 
						|
 | 
						|
    def __iadd__(fn):
 | 
						|
        def __iadd__(self, iterable):
 | 
						|
            # list.__iadd__ takes any iterable and seems to let TypeError
 | 
						|
            # raise as-is instead of returning NotImplemented
 | 
						|
            for value in list(iterable):
 | 
						|
                self.append(value)
 | 
						|
            return self
 | 
						|
 | 
						|
        _tidy(__iadd__)
 | 
						|
        return __iadd__
 | 
						|
 | 
						|
    def pop(fn):
 | 
						|
        def pop(self, index=-1):
 | 
						|
            __before_pop(self)
 | 
						|
            item = fn(self, index)
 | 
						|
            __del(self, item)
 | 
						|
            return item
 | 
						|
 | 
						|
        _tidy(pop)
 | 
						|
        return pop
 | 
						|
 | 
						|
    if not util.py2k:
 | 
						|
 | 
						|
        def clear(fn):
 | 
						|
            def clear(self, index=-1):
 | 
						|
                for item in self:
 | 
						|
                    __del(self, item)
 | 
						|
                fn(self)
 | 
						|
 | 
						|
            _tidy(clear)
 | 
						|
            return clear
 | 
						|
 | 
						|
    # __imul__ : not wrapping this.  all members of the collection are already
 | 
						|
    # present, so no need to fire appends... wrapping it with an explicit
 | 
						|
    # decorator is still possible, so events on *= can be had if they're
 | 
						|
    # desired.  hard to imagine a use case for __imul__, though.
 | 
						|
 | 
						|
    l = locals().copy()
 | 
						|
    l.pop("_tidy")
 | 
						|
    return l
 | 
						|
 | 
						|
 | 
						|
def _dict_decorators():
 | 
						|
    """Tailored instrumentation wrappers for any dict-like mapping class."""
 | 
						|
 | 
						|
    def _tidy(fn):
 | 
						|
        fn._sa_instrumented = True
 | 
						|
        fn.__doc__ = getattr(dict, fn.__name__).__doc__
 | 
						|
 | 
						|
    Unspecified = util.symbol("Unspecified")
 | 
						|
 | 
						|
    def __setitem__(fn):
 | 
						|
        def __setitem__(self, key, value, _sa_initiator=None):
 | 
						|
            if key in self:
 | 
						|
                __del(self, self[key], _sa_initiator)
 | 
						|
            value = __set(self, value, _sa_initiator)
 | 
						|
            fn(self, key, value)
 | 
						|
 | 
						|
        _tidy(__setitem__)
 | 
						|
        return __setitem__
 | 
						|
 | 
						|
    def __delitem__(fn):
 | 
						|
        def __delitem__(self, key, _sa_initiator=None):
 | 
						|
            if key in self:
 | 
						|
                __del(self, self[key], _sa_initiator)
 | 
						|
            fn(self, key)
 | 
						|
 | 
						|
        _tidy(__delitem__)
 | 
						|
        return __delitem__
 | 
						|
 | 
						|
    def clear(fn):
 | 
						|
        def clear(self):
 | 
						|
            for key in self:
 | 
						|
                __del(self, self[key])
 | 
						|
            fn(self)
 | 
						|
 | 
						|
        _tidy(clear)
 | 
						|
        return clear
 | 
						|
 | 
						|
    def pop(fn):
 | 
						|
        def pop(self, key, default=Unspecified):
 | 
						|
            __before_pop(self)
 | 
						|
            _to_del = key in self
 | 
						|
            if default is Unspecified:
 | 
						|
                item = fn(self, key)
 | 
						|
            else:
 | 
						|
                item = fn(self, key, default)
 | 
						|
            if _to_del:
 | 
						|
                __del(self, item)
 | 
						|
            return item
 | 
						|
 | 
						|
        _tidy(pop)
 | 
						|
        return pop
 | 
						|
 | 
						|
    def popitem(fn):
 | 
						|
        def popitem(self):
 | 
						|
            __before_pop(self)
 | 
						|
            item = fn(self)
 | 
						|
            __del(self, item[1])
 | 
						|
            return item
 | 
						|
 | 
						|
        _tidy(popitem)
 | 
						|
        return popitem
 | 
						|
 | 
						|
    def setdefault(fn):
 | 
						|
        def setdefault(self, key, default=None):
 | 
						|
            if key not in self:
 | 
						|
                self.__setitem__(key, default)
 | 
						|
                return default
 | 
						|
            else:
 | 
						|
                value = self.__getitem__(key)
 | 
						|
                if value is default:
 | 
						|
                    __set_wo_mutation(self, value, None)
 | 
						|
 | 
						|
                return value
 | 
						|
 | 
						|
        _tidy(setdefault)
 | 
						|
        return setdefault
 | 
						|
 | 
						|
    def update(fn):
 | 
						|
        def update(self, __other=Unspecified, **kw):
 | 
						|
            if __other is not Unspecified:
 | 
						|
                if hasattr(__other, "keys"):
 | 
						|
                    for key in list(__other):
 | 
						|
                        if key not in self or self[key] is not __other[key]:
 | 
						|
                            self[key] = __other[key]
 | 
						|
                        else:
 | 
						|
                            __set_wo_mutation(self, __other[key], None)
 | 
						|
                else:
 | 
						|
                    for key, value in __other:
 | 
						|
                        if key not in self or self[key] is not value:
 | 
						|
                            self[key] = value
 | 
						|
                        else:
 | 
						|
                            __set_wo_mutation(self, value, None)
 | 
						|
            for key in kw:
 | 
						|
                if key not in self or self[key] is not kw[key]:
 | 
						|
                    self[key] = kw[key]
 | 
						|
                else:
 | 
						|
                    __set_wo_mutation(self, kw[key], None)
 | 
						|
 | 
						|
        _tidy(update)
 | 
						|
        return update
 | 
						|
 | 
						|
    l = locals().copy()
 | 
						|
    l.pop("_tidy")
 | 
						|
    l.pop("Unspecified")
 | 
						|
    return l
 | 
						|
 | 
						|
 | 
						|
_set_binop_bases = (set, frozenset)
 | 
						|
 | 
						|
 | 
						|
def _set_binops_check_strict(self, obj):
 | 
						|
    """Allow only set, frozenset and self.__class__-derived
 | 
						|
    objects in binops."""
 | 
						|
    return isinstance(obj, _set_binop_bases + (self.__class__,))
 | 
						|
 | 
						|
 | 
						|
def _set_binops_check_loose(self, obj):
 | 
						|
    """Allow anything set-like to participate in set binops."""
 | 
						|
    return (
 | 
						|
        isinstance(obj, _set_binop_bases + (self.__class__,))
 | 
						|
        or util.duck_type_collection(obj) == set
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def _set_decorators():
 | 
						|
    """Tailored instrumentation wrappers for any set-like class."""
 | 
						|
 | 
						|
    def _tidy(fn):
 | 
						|
        fn._sa_instrumented = True
 | 
						|
        fn.__doc__ = getattr(set, fn.__name__).__doc__
 | 
						|
 | 
						|
    Unspecified = util.symbol("Unspecified")
 | 
						|
 | 
						|
    def add(fn):
 | 
						|
        def add(self, value, _sa_initiator=None):
 | 
						|
            if value not in self:
 | 
						|
                value = __set(self, value, _sa_initiator)
 | 
						|
            else:
 | 
						|
                __set_wo_mutation(self, value, _sa_initiator)
 | 
						|
            # testlib.pragma exempt:__hash__
 | 
						|
            fn(self, value)
 | 
						|
 | 
						|
        _tidy(add)
 | 
						|
        return add
 | 
						|
 | 
						|
    def discard(fn):
 | 
						|
        def discard(self, value, _sa_initiator=None):
 | 
						|
            # testlib.pragma exempt:__hash__
 | 
						|
            if value in self:
 | 
						|
                __del(self, value, _sa_initiator)
 | 
						|
                # testlib.pragma exempt:__hash__
 | 
						|
            fn(self, value)
 | 
						|
 | 
						|
        _tidy(discard)
 | 
						|
        return discard
 | 
						|
 | 
						|
    def remove(fn):
 | 
						|
        def remove(self, value, _sa_initiator=None):
 | 
						|
            # testlib.pragma exempt:__hash__
 | 
						|
            if value in self:
 | 
						|
                __del(self, value, _sa_initiator)
 | 
						|
            # testlib.pragma exempt:__hash__
 | 
						|
            fn(self, value)
 | 
						|
 | 
						|
        _tidy(remove)
 | 
						|
        return remove
 | 
						|
 | 
						|
    def pop(fn):
 | 
						|
        def pop(self):
 | 
						|
            __before_pop(self)
 | 
						|
            item = fn(self)
 | 
						|
            # for set in particular, we have no way to access the item
 | 
						|
            # that will be popped before pop is called.
 | 
						|
            __del(self, item)
 | 
						|
            return item
 | 
						|
 | 
						|
        _tidy(pop)
 | 
						|
        return pop
 | 
						|
 | 
						|
    def clear(fn):
 | 
						|
        def clear(self):
 | 
						|
            for item in list(self):
 | 
						|
                self.remove(item)
 | 
						|
 | 
						|
        _tidy(clear)
 | 
						|
        return clear
 | 
						|
 | 
						|
    def update(fn):
 | 
						|
        def update(self, value):
 | 
						|
            for item in value:
 | 
						|
                self.add(item)
 | 
						|
 | 
						|
        _tidy(update)
 | 
						|
        return update
 | 
						|
 | 
						|
    def __ior__(fn):
 | 
						|
        def __ior__(self, value):
 | 
						|
            if not _set_binops_check_strict(self, value):
 | 
						|
                return NotImplemented
 | 
						|
            for item in value:
 | 
						|
                self.add(item)
 | 
						|
            return self
 | 
						|
 | 
						|
        _tidy(__ior__)
 | 
						|
        return __ior__
 | 
						|
 | 
						|
    def difference_update(fn):
 | 
						|
        def difference_update(self, value):
 | 
						|
            for item in value:
 | 
						|
                self.discard(item)
 | 
						|
 | 
						|
        _tidy(difference_update)
 | 
						|
        return difference_update
 | 
						|
 | 
						|
    def __isub__(fn):
 | 
						|
        def __isub__(self, value):
 | 
						|
            if not _set_binops_check_strict(self, value):
 | 
						|
                return NotImplemented
 | 
						|
            for item in value:
 | 
						|
                self.discard(item)
 | 
						|
            return self
 | 
						|
 | 
						|
        _tidy(__isub__)
 | 
						|
        return __isub__
 | 
						|
 | 
						|
    def intersection_update(fn):
 | 
						|
        def intersection_update(self, other):
 | 
						|
            want, have = self.intersection(other), set(self)
 | 
						|
            remove, add = have - want, want - have
 | 
						|
 | 
						|
            for item in remove:
 | 
						|
                self.remove(item)
 | 
						|
            for item in add:
 | 
						|
                self.add(item)
 | 
						|
 | 
						|
        _tidy(intersection_update)
 | 
						|
        return intersection_update
 | 
						|
 | 
						|
    def __iand__(fn):
 | 
						|
        def __iand__(self, other):
 | 
						|
            if not _set_binops_check_strict(self, other):
 | 
						|
                return NotImplemented
 | 
						|
            want, have = self.intersection(other), set(self)
 | 
						|
            remove, add = have - want, want - have
 | 
						|
 | 
						|
            for item in remove:
 | 
						|
                self.remove(item)
 | 
						|
            for item in add:
 | 
						|
                self.add(item)
 | 
						|
            return self
 | 
						|
 | 
						|
        _tidy(__iand__)
 | 
						|
        return __iand__
 | 
						|
 | 
						|
    def symmetric_difference_update(fn):
 | 
						|
        def symmetric_difference_update(self, other):
 | 
						|
            want, have = self.symmetric_difference(other), set(self)
 | 
						|
            remove, add = have - want, want - have
 | 
						|
 | 
						|
            for item in remove:
 | 
						|
                self.remove(item)
 | 
						|
            for item in add:
 | 
						|
                self.add(item)
 | 
						|
 | 
						|
        _tidy(symmetric_difference_update)
 | 
						|
        return symmetric_difference_update
 | 
						|
 | 
						|
    def __ixor__(fn):
 | 
						|
        def __ixor__(self, other):
 | 
						|
            if not _set_binops_check_strict(self, other):
 | 
						|
                return NotImplemented
 | 
						|
            want, have = self.symmetric_difference(other), set(self)
 | 
						|
            remove, add = have - want, want - have
 | 
						|
 | 
						|
            for item in remove:
 | 
						|
                self.remove(item)
 | 
						|
            for item in add:
 | 
						|
                self.add(item)
 | 
						|
            return self
 | 
						|
 | 
						|
        _tidy(__ixor__)
 | 
						|
        return __ixor__
 | 
						|
 | 
						|
    l = locals().copy()
 | 
						|
    l.pop("_tidy")
 | 
						|
    l.pop("Unspecified")
 | 
						|
    return l
 | 
						|
 | 
						|
 | 
						|
class InstrumentedList(list):
 | 
						|
    """An instrumented version of the built-in list."""
 | 
						|
 | 
						|
 | 
						|
class InstrumentedSet(set):
 | 
						|
    """An instrumented version of the built-in set."""
 | 
						|
 | 
						|
 | 
						|
class InstrumentedDict(dict):
 | 
						|
    """An instrumented version of the built-in dict."""
 | 
						|
 | 
						|
 | 
						|
__canned_instrumentation = {
 | 
						|
    list: InstrumentedList,
 | 
						|
    set: InstrumentedSet,
 | 
						|
    dict: InstrumentedDict,
 | 
						|
}
 | 
						|
 | 
						|
__interfaces = {
 | 
						|
    list: (
 | 
						|
        {"appender": "append", "remover": "remove", "iterator": "__iter__"},
 | 
						|
        _list_decorators(),
 | 
						|
    ),
 | 
						|
    set: (
 | 
						|
        {"appender": "add", "remover": "remove", "iterator": "__iter__"},
 | 
						|
        _set_decorators(),
 | 
						|
    ),
 | 
						|
    # decorators are required for dicts and object collections.
 | 
						|
    dict: ({"iterator": "values"}, _dict_decorators())
 | 
						|
    if util.py3k
 | 
						|
    else ({"iterator": "itervalues"}, _dict_decorators()),
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
class MappedCollection(dict):
 | 
						|
    """A basic dictionary-based collection class.
 | 
						|
 | 
						|
    Extends dict with the minimal bag semantics that collection
 | 
						|
    classes require. ``set`` and ``remove`` are implemented in terms
 | 
						|
    of a keying function: any callable that takes an object and
 | 
						|
    returns an object for use as a dictionary key.
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, keyfunc):
 | 
						|
        """Create a new collection with keying provided by keyfunc.
 | 
						|
 | 
						|
        keyfunc may be any callable that takes an object and returns an object
 | 
						|
        for use as a dictionary key.
 | 
						|
 | 
						|
        The keyfunc will be called every time the ORM needs to add a member by
 | 
						|
        value-only (such as when loading instances from the database) or
 | 
						|
        remove a member.  The usual cautions about dictionary keying apply-
 | 
						|
        ``keyfunc(object)`` should return the same output for the life of the
 | 
						|
        collection.  Keying based on mutable properties can result in
 | 
						|
        unreachable instances "lost" in the collection.
 | 
						|
 | 
						|
        """
 | 
						|
        self.keyfunc = keyfunc
 | 
						|
 | 
						|
    @collection.appender
 | 
						|
    @collection.internally_instrumented
 | 
						|
    def set(self, value, _sa_initiator=None):
 | 
						|
        """Add an item by value, consulting the keyfunc for the key."""
 | 
						|
 | 
						|
        key = self.keyfunc(value)
 | 
						|
        self.__setitem__(key, value, _sa_initiator)
 | 
						|
 | 
						|
    @collection.remover
 | 
						|
    @collection.internally_instrumented
 | 
						|
    def remove(self, value, _sa_initiator=None):
 | 
						|
        """Remove an item by value, consulting the keyfunc for the key."""
 | 
						|
 | 
						|
        key = self.keyfunc(value)
 | 
						|
        # Let self[key] raise if key is not in this collection
 | 
						|
        # testlib.pragma exempt:__ne__
 | 
						|
        if self[key] != value:
 | 
						|
            raise sa_exc.InvalidRequestError(
 | 
						|
                "Can not remove '%s': collection holds '%s' for key '%s'. "
 | 
						|
                "Possible cause: is the MappedCollection key function "
 | 
						|
                "based on mutable properties or properties that only obtain "
 | 
						|
                "values after flush?" % (value, self[key], key)
 | 
						|
            )
 | 
						|
        self.__delitem__(key, _sa_initiator)
 | 
						|
 | 
						|
 | 
						|
# ensure instrumentation is associated with
 | 
						|
# these built-in classes; if a user-defined class
 | 
						|
# subclasses these and uses @internally_instrumented,
 | 
						|
# the superclass is otherwise not instrumented.
 | 
						|
# see [ticket:2406].
 | 
						|
_instrument_class(MappedCollection)
 | 
						|
_instrument_class(InstrumentedList)
 | 
						|
_instrument_class(InstrumentedSet)
 |