1# orm/collections.py 
    2# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors 
    3# <see AUTHORS file> 
    4# 
    5# This module is part of SQLAlchemy and is released under 
    6# the MIT License: https://www.opensource.org/licenses/mit-license.php 
    7# mypy: allow-untyped-defs, allow-untyped-calls 
    8 
    9"""Support for collections of mapped entities. 
    10 
    11The collections package supplies the machinery used to inform the ORM of 
    12collection membership changes.  An instrumentation via decoration approach is 
    13used, allowing arbitrary types (including built-ins) to be used as entity 
    14collections without requiring inheritance from a base class. 
    15 
    16Instrumentation decoration relays membership change events to the 
    17:class:`.CollectionAttributeImpl` that is currently managing the collection. 
    18The decorators observe function call arguments and return values, tracking 
    19entities entering or leaving the collection.  Two decorator approaches are 
    20provided.  One is a bundle of generic decorators that map function arguments 
    21and return values to events:: 
    22 
    23  from sqlalchemy.orm.collections import collection 
    24 
    25 
    26  class MyClass: 
    27      # ... 
    28 
    29      @collection.adds(1) 
    30      def store(self, item): 
    31          self.data.append(item) 
    32 
    33      @collection.removes_return() 
    34      def pop(self): 
    35          return self.data.pop() 
    36 
    37The second approach is a bundle of targeted decorators that wrap appropriate 
    38append and remove notifiers around the mutation methods present in the 
    39standard Python ``list``, ``set`` and ``dict`` interfaces.  These could be 
    40specified in terms of generic decorator recipes, but are instead hand-tooled 
    41for increased efficiency.  The targeted decorators occasionally implement 
    42adapter-like behavior, such as mapping bulk-set methods (``extend``, 
    43``update``, ``__setslice__``, etc.) into the series of atomic mutation events 
    44that the ORM requires. 
    45 
    46The targeted decorators are used internally for automatic instrumentation of 
    47entity collection classes.  Every collection class goes through a 
    48transformation process roughly like so: 
    49 
    501. If the class is a built-in, substitute a trivial sub-class 
    512. Is this class already instrumented? 
    523. Add in generic decorators 
    534. Sniff out the collection interface through duck-typing 
    545. Add targeted decoration to any undecorated interface method 
    55 
    56This process modifies the class at runtime, decorating methods and adding some 
    57bookkeeping properties.  This isn't possible (or desirable) for built-in 
    58classes like ``list``, so trivial sub-classes are substituted to hold 
    59decoration:: 
    60 
    61  class InstrumentedList(list): 
    62      pass 
    63 
    64Collection classes can be specified in ``relationship(collection_class=)`` as 
    65types or a function that returns an instance.  Collection classes are 
    66inspected and instrumented during the mapper compilation phase.  The 
    67collection_class callable will be executed once to produce a specimen 
    68instance, and the type of that specimen will be instrumented.  Functions that 
    69return built-in types like ``lists`` will be adapted to produce instrumented 
    70instances. 
    71 
    72When extending a known type like ``list``, additional decorations are not 
    73generally not needed.  Odds are, the extension method will delegate to a 
    74method that's already instrumented.  For example:: 
    75 
    76  class QueueIsh(list): 
    77      def push(self, item): 
    78          self.append(item) 
    79 
    80      def shift(self): 
    81          return self.pop(0) 
    82 
    83There's no need to decorate these methods.  ``append`` and ``pop`` are already 
    84instrumented as part of the ``list`` interface.  Decorating them would fire 
    85duplicate events, which should be avoided. 
    86 
    87The targeted decoration tries not to rely on other methods in the underlying 
    88collection class, but some are unavoidable.  Many depend on 'read' methods 
    89being present to properly instrument a 'write', for example, ``__setitem__`` 
    90needs ``__getitem__``.  "Bulk" methods like ``update`` and ``extend`` may also 
    91reimplemented in terms of atomic appends and removes, so the ``extend`` 
    92decoration will actually perform many ``append`` operations and not call the 
    93underlying method at all. 
    94 
    95Tight control over bulk operation and the firing of events is also possible by 
    96implementing the instrumentation internally in your methods.  The basic 
    97instrumentation package works under the general assumption that collection 
    98mutation will not raise unusual exceptions.  If you want to closely 
    99orchestrate append and remove events with exception management, internal 
    100instrumentation may be the answer.  Within your method, 
    101``collection_adapter(self)`` will retrieve an object that you can use for 
    102explicit control over triggering append and remove events. 
    103 
    104The owning object and :class:`.CollectionAttributeImpl` are also reachable 
    105through the adapter, allowing for some very sophisticated behavior. 
    106 
    107""" 
    108from __future__ import annotations 
    109 
    110import operator 
    111import threading 
    112import typing 
    113from typing import Any 
    114from typing import Callable 
    115from typing import cast 
    116from typing import Collection 
    117from typing import Dict 
    118from typing import Iterable 
    119from typing import List 
    120from typing import NoReturn 
    121from typing import Optional 
    122from typing import Protocol 
    123from typing import Set 
    124from typing import Tuple 
    125from typing import Type 
    126from typing import TYPE_CHECKING 
    127from typing import TypeVar 
    128from typing import Union 
    129import weakref 
    130 
    131from .base import NO_KEY 
    132from .. import exc as sa_exc 
    133from .. import util 
    134from ..sql.base import NO_ARG 
    135from ..util.compat import inspect_getfullargspec 
    136 
    137if typing.TYPE_CHECKING: 
    138    from .attributes import _CollectionAttributeImpl 
    139    from .attributes import AttributeEventToken 
    140    from .mapped_collection import attribute_keyed_dict 
    141    from .mapped_collection import column_keyed_dict 
    142    from .mapped_collection import keyfunc_mapping 
    143    from .mapped_collection import KeyFuncDict  # noqa: F401 
    144    from .state import InstanceState 
    145 
    146 
    147__all__ = [ 
    148    "collection", 
    149    "collection_adapter", 
    150    "keyfunc_mapping", 
    151    "column_keyed_dict", 
    152    "attribute_keyed_dict", 
    153    "KeyFuncDict", 
    154    # old names in < 2.0 
    155    "mapped_collection", 
    156    "column_mapped_collection", 
    157    "attribute_mapped_collection", 
    158    "MappedCollection", 
    159] 
    160 
    161__instrumentation_mutex = threading.Lock() 
    162 
    163 
    164_CollectionFactoryType = Callable[[], "_AdaptedCollectionProtocol"] 
    165 
    166_T = TypeVar("_T", bound=Any) 
    167_KT = TypeVar("_KT", bound=Any) 
    168_VT = TypeVar("_VT", bound=Any) 
    169_COL = TypeVar("_COL", bound="Collection[Any]") 
    170_FN = TypeVar("_FN", bound="Callable[..., Any]") 
    171 
    172 
    173class _CollectionConverterProtocol(Protocol): 
    174    def __call__(self, collection: _COL) -> _COL: ... 
    175 
    176 
    177class _AdaptedCollectionProtocol(Protocol): 
    178    _sa_adapter: CollectionAdapter 
    179    _sa_appender: Callable[..., Any] 
    180    _sa_remover: Callable[..., Any] 
    181    _sa_iterator: Callable[..., Iterable[Any]] 
    182 
    183 
    184class collection: 
    185    """Decorators for entity collection classes. 
    186 
    187    The decorators fall into two groups: annotations and interception recipes. 
    188 
    189    The annotating decorators (appender, remover, iterator, 
    190    internally_instrumented) indicate the method's purpose and take no 
    191    arguments.  They are not written with parens:: 
    192 
    193        @collection.appender 
    194        def append(self, append): ... 
    195 
    196    The recipe decorators all require parens, even those that take no 
    197    arguments:: 
    198 
    199        @collection.adds("entity") 
    200        def insert(self, position, entity): ... 
    201 
    202 
    203        @collection.removes_return() 
    204        def popitem(self): ... 
    205 
    206    """ 
    207 
    208    # Bundled as a class solely for ease of use: packaging, doc strings, 
    209    # importability. 
    210 
    211    @staticmethod 
    212    def appender(fn): 
    213        """Tag the method as the collection appender. 
    214 
    215        The appender method is called with one positional argument: the value 
    216        to append. The method will be automatically decorated with 'adds(1)' 
    217        if not already decorated:: 
    218 
    219            @collection.appender 
    220            def add(self, append): ... 
    221 
    222 
    223            # or, equivalently 
    224            @collection.appender 
    225            @collection.adds(1) 
    226            def add(self, append): ... 
    227 
    228 
    229            # for mapping type, an 'append' may kick out a previous value 
    230            # that occupies that slot.  consider d['a'] = 'foo'- any previous 
    231            # value in d['a'] is discarded. 
    232            @collection.appender 
    233            @collection.replaces(1) 
    234            def add(self, entity): 
    235                key = some_key_func(entity) 
    236                previous = None 
    237                if key in self: 
    238                    previous = self[key] 
    239                self[key] = entity 
    240                return previous 
    241 
    242        If the value to append is not allowed in the collection, you may 
    243        raise an exception.  Something to remember is that the appender 
    244        will be called for each object mapped by a database query.  If the 
    245        database contains rows that violate your collection semantics, you 
    246        will need to get creative to fix the problem, as access via the 
    247        collection will not work. 
    248 
    249        If the appender method is internally instrumented, you must also 
    250        receive the keyword argument '_sa_initiator' and ensure its 
    251        promulgation to collection events. 
    252 
    253        """ 
    254        fn._sa_instrument_role = "appender" 
    255        return fn 
    256 
    257    @staticmethod 
    258    def remover(fn): 
    259        """Tag the method as the collection remover. 
    260 
    261        The remover method is called with one positional argument: the value 
    262        to remove. The method will be automatically decorated with 
    263        :meth:`removes_return` if not already decorated:: 
    264 
    265            @collection.remover 
    266            def zap(self, entity): ... 
    267 
    268 
    269            # or, equivalently 
    270            @collection.remover 
    271            @collection.removes_return() 
    272            def zap(self): ... 
    273 
    274        If the value to remove is not present in the collection, you may 
    275        raise an exception or return None to ignore the error. 
    276 
    277        If the remove method is internally instrumented, you must also 
    278        receive the keyword argument '_sa_initiator' and ensure its 
    279        promulgation to collection events. 
    280 
    281        """ 
    282        fn._sa_instrument_role = "remover" 
    283        return fn 
    284 
    285    @staticmethod 
    286    def iterator(fn): 
    287        """Tag the method as the collection remover. 
    288 
    289        The iterator method is called with no arguments.  It is expected to 
    290        return an iterator over all collection members:: 
    291 
    292            @collection.iterator 
    293            def __iter__(self): ... 
    294 
    295        """ 
    296        fn._sa_instrument_role = "iterator" 
    297        return fn 
    298 
    299    @staticmethod 
    300    def internally_instrumented(fn): 
    301        """Tag the method as instrumented. 
    302 
    303        This tag will prevent any decoration from being applied to the 
    304        method. Use this if you are orchestrating your own calls to 
    305        :func:`.collection_adapter` in one of the basic SQLAlchemy 
    306        interface methods, or to prevent an automatic ABC method 
    307        decoration from wrapping your implementation:: 
    308 
    309            # normally an 'extend' method on a list-like class would be 
    310            # automatically intercepted and re-implemented in terms of 
    311            # SQLAlchemy events and append().  your implementation will 
    312            # never be called, unless: 
    313            @collection.internally_instrumented 
    314            def extend(self, items): ... 
    315 
    316        """ 
    317        fn._sa_instrumented = True 
    318        return fn 
    319 
    320    @staticmethod 
    321    def adds(arg: int) -> Callable[[_FN], _FN]: 
    322        """Mark the method as adding an entity to the collection. 
    323 
    324        Adds "add to collection" handling to the method.  The decorator 
    325        argument indicates which method argument holds the SQLAlchemy-relevant 
    326        value.  Arguments can be specified positionally (i.e. integer) or by 
    327        name:: 
    328 
    329            @collection.adds(1) 
    330            def push(self, item): ... 
    331 
    332 
    333            @collection.adds("entity") 
    334            def do_stuff(self, thing, entity=None): ... 
    335 
    336        """ 
    337 
    338        def decorator(fn): 
    339            fn._sa_instrument_before = ("fire_append_event", arg) 
    340            return fn 
    341 
    342        return decorator 
    343 
    344    @staticmethod 
    345    def replaces(arg): 
    346        """Mark the method as replacing an entity in the collection. 
    347 
    348        Adds "add to collection" and "remove from collection" handling to 
    349        the method.  The decorator argument indicates which method argument 
    350        holds the SQLAlchemy-relevant value to be added, and return value, if 
    351        any will be considered the value to remove. 
    352 
    353        Arguments can be specified positionally (i.e. integer) or by name:: 
    354 
    355            @collection.replaces(2) 
    356            def __setitem__(self, index, item): ... 
    357 
    358        """ 
    359 
    360        def decorator(fn): 
    361            fn._sa_instrument_before = ("fire_append_event", arg) 
    362            fn._sa_instrument_after = "fire_remove_event" 
    363            return fn 
    364 
    365        return decorator 
    366 
    367    @staticmethod 
    368    def removes(arg): 
    369        """Mark the method as removing an entity in the collection. 
    370 
    371        Adds "remove from collection" handling to the method.  The decorator 
    372        argument indicates which method argument holds the SQLAlchemy-relevant 
    373        value to be removed. Arguments can be specified positionally (i.e. 
    374        integer) or by name:: 
    375 
    376            @collection.removes(1) 
    377            def zap(self, item): ... 
    378 
    379        For methods where the value to remove is not known at call-time, use 
    380        collection.removes_return. 
    381 
    382        """ 
    383 
    384        def decorator(fn): 
    385            fn._sa_instrument_before = ("fire_remove_event", arg) 
    386            return fn 
    387 
    388        return decorator 
    389 
    390    @staticmethod 
    391    def removes_return(): 
    392        """Mark the method as removing an entity in the collection. 
    393 
    394        Adds "remove from collection" handling to the method.  The return 
    395        value of the method, if any, is considered the value to remove.  The 
    396        method arguments are not inspected:: 
    397 
    398            @collection.removes_return() 
    399            def pop(self): ... 
    400 
    401        For methods where the value to remove is known at call-time, use 
    402        collection.remove. 
    403 
    404        """ 
    405 
    406        def decorator(fn): 
    407            fn._sa_instrument_after = "fire_remove_event" 
    408            return fn 
    409 
    410        return decorator 
    411 
    412 
    413if TYPE_CHECKING: 
    414 
    415    def collection_adapter(collection: Collection[Any]) -> CollectionAdapter: 
    416        """Fetch the :class:`.CollectionAdapter` for a collection.""" 
    417 
    418else: 
    419    collection_adapter = operator.attrgetter("_sa_adapter") 
    420 
    421 
    422class CollectionAdapter: 
    423    """Bridges between the ORM and arbitrary Python collections. 
    424 
    425    Proxies base-level collection operations (append, remove, iterate) 
    426    to the underlying Python collection, and emits add/remove events for 
    427    entities entering or leaving the collection. 
    428 
    429    The ORM uses :class:`.CollectionAdapter` exclusively for interaction with 
    430    entity collections. 
    431 
    432 
    433    """ 
    434 
    435    __slots__ = ( 
    436        "attr", 
    437        "_key", 
    438        "_data", 
    439        "owner_state", 
    440        "invalidated", 
    441        "empty", 
    442    ) 
    443 
    444    attr: _CollectionAttributeImpl 
    445    _key: str 
    446 
    447    # this is actually a weakref; see note in constructor 
    448    _data: Callable[..., _AdaptedCollectionProtocol] 
    449 
    450    owner_state: InstanceState[Any] 
    451    invalidated: bool 
    452    empty: bool 
    453 
    454    def __init__( 
    455        self, 
    456        attr: _CollectionAttributeImpl, 
    457        owner_state: InstanceState[Any], 
    458        data: _AdaptedCollectionProtocol, 
    459    ): 
    460        self.attr = attr 
    461        self._key = attr.key 
    462 
    463        # this weakref stays referenced throughout the lifespan of 
    464        # CollectionAdapter.  so while the weakref can return None, this 
    465        # is realistically only during garbage collection of this object, so 
    466        # we type this as a callable that returns _AdaptedCollectionProtocol 
    467        # in all cases. 
    468        self._data = weakref.ref(data)  # type: ignore 
    469 
    470        self.owner_state = owner_state 
    471        data._sa_adapter = self 
    472        self.invalidated = False 
    473        self.empty = False 
    474 
    475    def _warn_invalidated(self) -> None: 
    476        util.warn("This collection has been invalidated.") 
    477 
    478    @property 
    479    def data(self) -> _AdaptedCollectionProtocol: 
    480        "The entity collection being adapted." 
    481        return self._data() 
    482 
    483    @property 
    484    def _referenced_by_owner(self) -> bool: 
    485        """return True if the owner state still refers to this collection. 
    486 
    487        This will return False within a bulk replace operation, 
    488        where this collection is the one being replaced. 
    489 
    490        """ 
    491        return self.owner_state.dict[self._key] is self._data() 
    492 
    493    def bulk_appender(self): 
    494        return self._data()._sa_appender 
    495 
    496    def append_with_event( 
    497        self, item: Any, initiator: Optional[AttributeEventToken] = None 
    498    ) -> None: 
    499        """Add an entity to the collection, firing mutation events.""" 
    500 
    501        self._data()._sa_appender(item, _sa_initiator=initiator) 
    502 
    503    def _set_empty(self, user_data): 
    504        assert ( 
    505            not self.empty 
    506        ), "This collection adapter is already in the 'empty' state" 
    507        self.empty = True 
    508        self.owner_state._empty_collections[self._key] = user_data 
    509 
    510    def _reset_empty(self) -> None: 
    511        assert ( 
    512            self.empty 
    513        ), "This collection adapter is not in the 'empty' state" 
    514        self.empty = False 
    515        self.owner_state.dict[self._key] = ( 
    516            self.owner_state._empty_collections.pop(self._key) 
    517        ) 
    518 
    519    def _refuse_empty(self) -> NoReturn: 
    520        raise sa_exc.InvalidRequestError( 
    521            "This is a special 'empty' collection which cannot accommodate " 
    522            "internal mutation operations" 
    523        ) 
    524 
    525    def append_without_event(self, item: Any) -> None: 
    526        """Add or restore an entity to the collection, firing no events.""" 
    527 
    528        if self.empty: 
    529            self._refuse_empty() 
    530        self._data()._sa_appender(item, _sa_initiator=False) 
    531 
    532    def append_multiple_without_event(self, items: Iterable[Any]) -> None: 
    533        """Add or restore an entity to the collection, firing no events.""" 
    534        if self.empty: 
    535            self._refuse_empty() 
    536        appender = self._data()._sa_appender 
    537        for item in items: 
    538            appender(item, _sa_initiator=False) 
    539 
    540    def bulk_remover(self): 
    541        return self._data()._sa_remover 
    542 
    543    def remove_with_event( 
    544        self, item: Any, initiator: Optional[AttributeEventToken] = None 
    545    ) -> None: 
    546        """Remove an entity from the collection, firing mutation events.""" 
    547        self._data()._sa_remover(item, _sa_initiator=initiator) 
    548 
    549    def remove_without_event(self, item: Any) -> None: 
    550        """Remove an entity from the collection, firing no events.""" 
    551        if self.empty: 
    552            self._refuse_empty() 
    553        self._data()._sa_remover(item, _sa_initiator=False) 
    554 
    555    def clear_with_event( 
    556        self, initiator: Optional[AttributeEventToken] = None 
    557    ) -> None: 
    558        """Empty the collection, firing a mutation event for each entity.""" 
    559 
    560        if self.empty: 
    561            self._refuse_empty() 
    562        remover = self._data()._sa_remover 
    563        for item in list(self): 
    564            remover(item, _sa_initiator=initiator) 
    565 
    566    def clear_without_event(self) -> None: 
    567        """Empty the collection, firing no events.""" 
    568 
    569        if self.empty: 
    570            self._refuse_empty() 
    571        remover = self._data()._sa_remover 
    572        for item in list(self): 
    573            remover(item, _sa_initiator=False) 
    574 
    575    def __iter__(self): 
    576        """Iterate over entities in the collection.""" 
    577 
    578        return iter(self._data()._sa_iterator()) 
    579 
    580    def __len__(self): 
    581        """Count entities in the collection.""" 
    582        return len(list(self._data()._sa_iterator())) 
    583 
    584    def __bool__(self): 
    585        return True 
    586 
    587    def _fire_append_wo_mutation_event_bulk( 
    588        self, items, initiator=None, key=NO_KEY 
    589    ): 
    590        if not items: 
    591            return 
    592 
    593        if initiator is not False: 
    594            if self.invalidated: 
    595                self._warn_invalidated() 
    596 
    597            if self.empty: 
    598                self._reset_empty() 
    599 
    600            for item in items: 
    601                self.attr.fire_append_wo_mutation_event( 
    602                    self.owner_state, 
    603                    self.owner_state.dict, 
    604                    item, 
    605                    initiator, 
    606                    key, 
    607                ) 
    608 
    609    def fire_append_wo_mutation_event(self, item, initiator=None, key=NO_KEY): 
    610        """Notify that a entity is entering the collection but is already 
    611        present. 
    612 
    613 
    614        Initiator is a token owned by the InstrumentedAttribute that 
    615        initiated the membership mutation, and should be left as None 
    616        unless you are passing along an initiator value from a chained 
    617        operation. 
    618 
    619        .. versionadded:: 1.4.15 
    620 
    621        """ 
    622        if initiator is not False: 
    623            if self.invalidated: 
    624                self._warn_invalidated() 
    625 
    626            if self.empty: 
    627                self._reset_empty() 
    628 
    629            return self.attr.fire_append_wo_mutation_event( 
    630                self.owner_state, self.owner_state.dict, item, initiator, key 
    631            ) 
    632        else: 
    633            return item 
    634 
    635    def fire_append_event(self, item, initiator=None, key=NO_KEY): 
    636        """Notify that a entity has entered the collection. 
    637 
    638        Initiator is a token owned by the InstrumentedAttribute that 
    639        initiated the membership mutation, and should be left as None 
    640        unless you are passing along an initiator value from a chained 
    641        operation. 
    642 
    643        """ 
    644        if initiator is not False: 
    645            if self.invalidated: 
    646                self._warn_invalidated() 
    647 
    648            if self.empty: 
    649                self._reset_empty() 
    650 
    651            return self.attr.fire_append_event( 
    652                self.owner_state, self.owner_state.dict, item, initiator, key 
    653            ) 
    654        else: 
    655            return item 
    656 
    657    def _fire_remove_event_bulk(self, items, initiator=None, key=NO_KEY): 
    658        if not items: 
    659            return 
    660 
    661        if initiator is not False: 
    662            if self.invalidated: 
    663                self._warn_invalidated() 
    664 
    665            if self.empty: 
    666                self._reset_empty() 
    667 
    668            for item in items: 
    669                self.attr.fire_remove_event( 
    670                    self.owner_state, 
    671                    self.owner_state.dict, 
    672                    item, 
    673                    initiator, 
    674                    key, 
    675                ) 
    676 
    677    def fire_remove_event(self, item, initiator=None, key=NO_KEY): 
    678        """Notify that a entity has been removed from the collection. 
    679 
    680        Initiator is the InstrumentedAttribute that initiated the membership 
    681        mutation, and should be left as None unless you are passing along 
    682        an initiator value from a chained operation. 
    683 
    684        """ 
    685        if initiator is not False: 
    686            if self.invalidated: 
    687                self._warn_invalidated() 
    688 
    689            if self.empty: 
    690                self._reset_empty() 
    691 
    692            self.attr.fire_remove_event( 
    693                self.owner_state, self.owner_state.dict, item, initiator, key 
    694            ) 
    695 
    696    def fire_pre_remove_event(self, initiator=None, key=NO_KEY): 
    697        """Notify that an entity is about to be removed from the collection. 
    698 
    699        Only called if the entity cannot be removed after calling 
    700        fire_remove_event(). 
    701 
    702        """ 
    703        if self.invalidated: 
    704            self._warn_invalidated() 
    705        self.attr.fire_pre_remove_event( 
    706            self.owner_state, 
    707            self.owner_state.dict, 
    708            initiator=initiator, 
    709            key=key, 
    710        ) 
    711 
    712    def __getstate__(self): 
    713        return { 
    714            "key": self._key, 
    715            "owner_state": self.owner_state, 
    716            "owner_cls": self.owner_state.class_, 
    717            "data": self.data, 
    718            "invalidated": self.invalidated, 
    719            "empty": self.empty, 
    720        } 
    721 
    722    def __setstate__(self, d): 
    723        self._key = d["key"] 
    724        self.owner_state = d["owner_state"] 
    725 
    726        # see note in constructor regarding this type: ignore 
    727        self._data = weakref.ref(d["data"])  # type: ignore 
    728 
    729        d["data"]._sa_adapter = self 
    730        self.invalidated = d["invalidated"] 
    731        self.attr = getattr(d["owner_cls"], self._key).impl 
    732        self.empty = d.get("empty", False) 
    733 
    734 
    735def bulk_replace(values, existing_adapter, new_adapter, initiator=None): 
    736    """Load a new collection, firing events based on prior like membership. 
    737 
    738    Appends instances in ``values`` onto the ``new_adapter``. Events will be 
    739    fired for any instance not present in the ``existing_adapter``.  Any 
    740    instances in ``existing_adapter`` not present in ``values`` will have 
    741    remove events fired upon them. 
    742 
    743    :param values: An iterable of collection member instances 
    744 
    745    :param existing_adapter: A :class:`.CollectionAdapter` of 
    746     instances to be replaced 
    747 
    748    :param new_adapter: An empty :class:`.CollectionAdapter` 
    749     to load with ``values`` 
    750 
    751 
    752    """ 
    753 
    754    assert isinstance(values, list) 
    755 
    756    idset = util.IdentitySet 
    757    existing_idset = idset(existing_adapter or ()) 
    758    constants = existing_idset.intersection(values or ()) 
    759    additions = idset(values or ()).difference(constants) 
    760    removals = existing_idset.difference(constants) 
    761 
    762    appender = new_adapter.bulk_appender() 
    763 
    764    for member in values or (): 
    765        if member in additions: 
    766            appender(member, _sa_initiator=initiator) 
    767        elif member in constants: 
    768            appender(member, _sa_initiator=False) 
    769 
    770    if existing_adapter: 
    771        existing_adapter._fire_append_wo_mutation_event_bulk( 
    772            constants, initiator=initiator 
    773        ) 
    774        existing_adapter._fire_remove_event_bulk(removals, initiator=initiator) 
    775 
    776 
    777def _prepare_instrumentation( 
    778    factory: Union[Type[Collection[Any]], _CollectionFactoryType], 
    779) -> _CollectionFactoryType: 
    780    """Prepare a callable for future use as a collection class factory. 
    781 
    782    Given a collection class factory (either a type or no-arg callable), 
    783    return another factory that will produce compatible instances when 
    784    called. 
    785 
    786    This function is responsible for converting collection_class=list 
    787    into the run-time behavior of collection_class=InstrumentedList. 
    788 
    789    """ 
    790 
    791    impl_factory: _CollectionFactoryType 
    792 
    793    # Convert a builtin to 'Instrumented*' 
    794    if factory in __canned_instrumentation: 
    795        impl_factory = __canned_instrumentation[factory] 
    796    else: 
    797        impl_factory = cast(_CollectionFactoryType, factory) 
    798 
    799    cls: Union[_CollectionFactoryType, Type[Collection[Any]]] 
    800 
    801    # Create a specimen 
    802    cls = type(impl_factory()) 
    803 
    804    # Did factory callable return a builtin? 
    805    if cls in __canned_instrumentation: 
    806        # if so, just convert. 
    807        # in previous major releases, this codepath wasn't working and was 
    808        # not covered by tests.   prior to that it supplied a "wrapper" 
    809        # function that would return the class, though the rationale for this 
    810        # case is not known 
    811        impl_factory = __canned_instrumentation[cls] 
    812        cls = type(impl_factory()) 
    813 
    814    # Instrument the class if needed. 
    815    if __instrumentation_mutex.acquire(): 
    816        try: 
    817            if getattr(cls, "_sa_instrumented", None) != id(cls): 
    818                _instrument_class(cls) 
    819        finally: 
    820            __instrumentation_mutex.release() 
    821 
    822    return impl_factory 
    823 
    824 
    825def _instrument_class(cls): 
    826    """Modify methods in a class and install instrumentation.""" 
    827 
    828    # In the normal call flow, a request for any of the 3 basic collection 
    829    # types is transformed into one of our trivial subclasses 
    830    # (e.g. InstrumentedList).  Catch anything else that sneaks in here... 
    831    if cls.__module__ == "__builtin__": 
    832        raise sa_exc.ArgumentError( 
    833            "Can not instrument a built-in type. Use a " 
    834            "subclass, even a trivial one." 
    835        ) 
    836 
    837    roles, methods = _locate_roles_and_methods(cls) 
    838 
    839    _setup_canned_roles(cls, roles, methods) 
    840 
    841    _assert_required_roles(cls, roles, methods) 
    842 
    843    _set_collection_attributes(cls, roles, methods) 
    844 
    845 
    846def _locate_roles_and_methods(cls): 
    847    """search for _sa_instrument_role-decorated methods in 
    848    method resolution order, assign to roles. 
    849 
    850    """ 
    851 
    852    roles: Dict[str, str] = {} 
    853    methods: Dict[str, Tuple[Optional[str], Optional[int], Optional[str]]] = {} 
    854 
    855    for supercls in cls.__mro__: 
    856        for name, method in vars(supercls).items(): 
    857            if not callable(method): 
    858                continue 
    859 
    860            # note role declarations 
    861            if hasattr(method, "_sa_instrument_role"): 
    862                role = method._sa_instrument_role 
    863                assert role in ("appender", "remover", "iterator") 
    864                roles.setdefault(role, name) 
    865 
    866            # transfer instrumentation requests from decorated function 
    867            # to the combined queue 
    868            before: Optional[Tuple[str, int]] = None 
    869            after: Optional[str] = None 
    870 
    871            if hasattr(method, "_sa_instrument_before"): 
    872                op, argument = method._sa_instrument_before 
    873                assert op in ("fire_append_event", "fire_remove_event") 
    874                before = op, argument 
    875            if hasattr(method, "_sa_instrument_after"): 
    876                op = method._sa_instrument_after 
    877                assert op in ("fire_append_event", "fire_remove_event") 
    878                after = op 
    879            if before: 
    880                methods[name] = before + (after,) 
    881            elif after: 
    882                methods[name] = None, None, after 
    883    return roles, methods 
    884 
    885 
    886def _setup_canned_roles(cls, roles, methods): 
    887    """see if this class has "canned" roles based on a known 
    888    collection type (dict, set, list).  Apply those roles 
    889    as needed to the "roles" dictionary, and also 
    890    prepare "decorator" methods 
    891 
    892    """ 
    893    collection_type = util.duck_type_collection(cls) 
    894    if collection_type in __interfaces: 
    895        assert collection_type is not None 
    896        canned_roles, decorators = __interfaces[collection_type] 
    897        for role, name in canned_roles.items(): 
    898            roles.setdefault(role, name) 
    899 
    900        # apply ABC auto-decoration to methods that need it 
    901        for method, decorator in decorators.items(): 
    902            fn = getattr(cls, method, None) 
    903            if ( 
    904                fn 
    905                and method not in methods 
    906                and not hasattr(fn, "_sa_instrumented") 
    907            ): 
    908                setattr(cls, method, decorator(fn)) 
    909 
    910 
    911def _assert_required_roles(cls, roles, methods): 
    912    """ensure all roles are present, and apply implicit instrumentation if 
    913    needed 
    914 
    915    """ 
    916    if "appender" not in roles or not hasattr(cls, roles["appender"]): 
    917        raise sa_exc.ArgumentError( 
    918            "Type %s must elect an appender method to be " 
    919            "a collection class" % cls.__name__ 
    920        ) 
    921    elif roles["appender"] not in methods and not hasattr( 
    922        getattr(cls, roles["appender"]), "_sa_instrumented" 
    923    ): 
    924        methods[roles["appender"]] = ("fire_append_event", 1, None) 
    925 
    926    if "remover" not in roles or not hasattr(cls, roles["remover"]): 
    927        raise sa_exc.ArgumentError( 
    928            "Type %s must elect a remover method to be " 
    929            "a collection class" % cls.__name__ 
    930        ) 
    931    elif roles["remover"] not in methods and not hasattr( 
    932        getattr(cls, roles["remover"]), "_sa_instrumented" 
    933    ): 
    934        methods[roles["remover"]] = ("fire_remove_event", 1, None) 
    935 
    936    if "iterator" not in roles or not hasattr(cls, roles["iterator"]): 
    937        raise sa_exc.ArgumentError( 
    938            "Type %s must elect an iterator method to be " 
    939            "a collection class" % cls.__name__ 
    940        ) 
    941 
    942 
    943def _set_collection_attributes(cls, roles, methods): 
    944    """apply ad-hoc instrumentation from decorators, class-level defaults 
    945    and implicit role declarations 
    946 
    947    """ 
    948    for method_name, (before, argument, after) in methods.items(): 
    949        setattr( 
    950            cls, 
    951            method_name, 
    952            _instrument_membership_mutator( 
    953                getattr(cls, method_name), before, argument, after 
    954            ), 
    955        ) 
    956    # intern the role map 
    957    for role, method_name in roles.items(): 
    958        setattr(cls, "_sa_%s" % role, getattr(cls, method_name)) 
    959 
    960    cls._sa_adapter = None 
    961 
    962    cls._sa_instrumented = id(cls) 
    963 
    964 
    965def _instrument_membership_mutator(method, before, argument, after): 
    966    """Route method args and/or return value through the collection 
    967    adapter.""" 
    968    # This isn't smart enough to handle @adds(1) for 'def fn(self, (a, b))' 
    969    if before: 
    970        fn_args = list( 
    971            util.flatten_iterator(inspect_getfullargspec(method)[0]) 
    972        ) 
    973        if isinstance(argument, int): 
    974            pos_arg = argument 
    975            named_arg = len(fn_args) > argument and fn_args[argument] or None 
    976        else: 
    977            if argument in fn_args: 
    978                pos_arg = fn_args.index(argument) 
    979            else: 
    980                pos_arg = None 
    981            named_arg = argument 
    982        del fn_args 
    983 
    984    def wrapper(*args, **kw): 
    985        if before: 
    986            if pos_arg is None: 
    987                if named_arg not in kw: 
    988                    raise sa_exc.ArgumentError( 
    989                        "Missing argument %s" % argument 
    990                    ) 
    991                value = kw[named_arg] 
    992            else: 
    993                if len(args) > pos_arg: 
    994                    value = args[pos_arg] 
    995                elif named_arg in kw: 
    996                    value = kw[named_arg] 
    997                else: 
    998                    raise sa_exc.ArgumentError( 
    999                        "Missing argument %s" % argument 
    1000                    ) 
    1001 
    1002        initiator = kw.pop("_sa_initiator", None) 
    1003        if initiator is False: 
    1004            executor = None 
    1005        else: 
    1006            executor = args[0]._sa_adapter 
    1007 
    1008        if before and executor: 
    1009            getattr(executor, before)(value, initiator) 
    1010 
    1011        if not after or not executor: 
    1012            return method(*args, **kw) 
    1013        else: 
    1014            res = method(*args, **kw) 
    1015            if res is not None: 
    1016                getattr(executor, after)(res, initiator) 
    1017            return res 
    1018 
    1019    wrapper._sa_instrumented = True  # type: ignore[attr-defined] 
    1020    if hasattr(method, "_sa_instrument_role"): 
    1021        wrapper._sa_instrument_role = method._sa_instrument_role  # type: ignore[attr-defined]  # noqa: E501 
    1022    wrapper.__name__ = method.__name__ 
    1023    wrapper.__doc__ = method.__doc__ 
    1024    return wrapper 
    1025 
    1026 
    1027def __set_wo_mutation(collection, item, _sa_initiator=None): 
    1028    """Run set wo mutation events. 
    1029 
    1030    The collection is not mutated. 
    1031 
    1032    """ 
    1033    if _sa_initiator is not False: 
    1034        executor = collection._sa_adapter 
    1035        if executor: 
    1036            executor.fire_append_wo_mutation_event( 
    1037                item, _sa_initiator, key=None 
    1038            ) 
    1039 
    1040 
    1041def __set(collection, item, _sa_initiator, key): 
    1042    """Run set events. 
    1043 
    1044    This event always occurs before the collection is actually mutated. 
    1045 
    1046    """ 
    1047 
    1048    if _sa_initiator is not False: 
    1049        executor = collection._sa_adapter 
    1050        if executor: 
    1051            item = executor.fire_append_event(item, _sa_initiator, key=key) 
    1052    return item 
    1053 
    1054 
    1055def __del(collection, item, _sa_initiator, key): 
    1056    """Run del events. 
    1057 
    1058    This event occurs before the collection is actually mutated, *except* 
    1059    in the case of a pop operation, in which case it occurs afterwards. 
    1060    For pop operations, the __before_pop hook is called before the 
    1061    operation occurs. 
    1062 
    1063    """ 
    1064    if _sa_initiator is not False: 
    1065        executor = collection._sa_adapter 
    1066        if executor: 
    1067            executor.fire_remove_event(item, _sa_initiator, key=key) 
    1068 
    1069 
    1070def __before_pop(collection, _sa_initiator=None): 
    1071    """An event which occurs on a before a pop() operation occurs.""" 
    1072    executor = collection._sa_adapter 
    1073    if executor: 
    1074        executor.fire_pre_remove_event(_sa_initiator) 
    1075 
    1076 
    1077def _list_decorators() -> Dict[str, Callable[[_FN], _FN]]: 
    1078    """Tailored instrumentation wrappers for any list-like class.""" 
    1079 
    1080    def _tidy(fn): 
    1081        fn._sa_instrumented = True 
    1082        fn.__doc__ = getattr(list, fn.__name__).__doc__ 
    1083 
    1084    def append(fn): 
    1085        def append(self, item, _sa_initiator=None): 
    1086            item = __set(self, item, _sa_initiator, NO_KEY) 
    1087            fn(self, item) 
    1088 
    1089        _tidy(append) 
    1090        return append 
    1091 
    1092    def remove(fn): 
    1093        def remove(self, value, _sa_initiator=None): 
    1094            __del(self, value, _sa_initiator, NO_KEY) 
    1095            # testlib.pragma exempt:__eq__ 
    1096            fn(self, value) 
    1097 
    1098        _tidy(remove) 
    1099        return remove 
    1100 
    1101    def insert(fn): 
    1102        def insert(self, index, value): 
    1103            value = __set(self, value, None, index) 
    1104            fn(self, index, value) 
    1105 
    1106        _tidy(insert) 
    1107        return insert 
    1108 
    1109    def __setitem__(fn): 
    1110        def __setitem__(self, index, value): 
    1111            if not isinstance(index, slice): 
    1112                existing = self[index] 
    1113                if existing is not None: 
    1114                    __del(self, existing, None, index) 
    1115                value = __set(self, value, None, index) 
    1116                fn(self, index, value) 
    1117            else: 
    1118                # slice assignment requires __delitem__, insert, __len__ 
    1119                step = index.step or 1 
    1120                start = index.start or 0 
    1121                if start < 0: 
    1122                    start += len(self) 
    1123                if index.stop is not None: 
    1124                    stop = index.stop 
    1125                else: 
    1126                    stop = len(self) 
    1127                if stop < 0: 
    1128                    stop += len(self) 
    1129 
    1130                if step == 1: 
    1131                    if value is self: 
    1132                        return 
    1133                    for i in range(start, stop, step): 
    1134                        if len(self) > start: 
    1135                            del self[start] 
    1136 
    1137                    for i, item in enumerate(value): 
    1138                        self.insert(i + start, item) 
    1139                else: 
    1140                    rng = list(range(start, stop, step)) 
    1141                    if len(value) != len(rng): 
    1142                        raise ValueError( 
    1143                            "attempt to assign sequence of size %s to " 
    1144                            "extended slice of size %s" 
    1145                            % (len(value), len(rng)) 
    1146                        ) 
    1147                    for i, item in zip(rng, value): 
    1148                        self.__setitem__(i, item) 
    1149 
    1150        _tidy(__setitem__) 
    1151        return __setitem__ 
    1152 
    1153    def __delitem__(fn): 
    1154        def __delitem__(self, index): 
    1155            if not isinstance(index, slice): 
    1156                item = self[index] 
    1157                __del(self, item, None, index) 
    1158                fn(self, index) 
    1159            else: 
    1160                # slice deletion requires __getslice__ and a slice-groking 
    1161                # __getitem__ for stepped deletion 
    1162                # note: not breaking this into atomic dels 
    1163                for item in self[index]: 
    1164                    __del(self, item, None, index) 
    1165                fn(self, index) 
    1166 
    1167        _tidy(__delitem__) 
    1168        return __delitem__ 
    1169 
    1170    def extend(fn): 
    1171        def extend(self, iterable): 
    1172            for value in list(iterable): 
    1173                self.append(value) 
    1174 
    1175        _tidy(extend) 
    1176        return extend 
    1177 
    1178    def __iadd__(fn): 
    1179        def __iadd__(self, iterable): 
    1180            # list.__iadd__ takes any iterable and seems to let TypeError 
    1181            # raise as-is instead of returning NotImplemented 
    1182            for value in list(iterable): 
    1183                self.append(value) 
    1184            return self 
    1185 
    1186        _tidy(__iadd__) 
    1187        return __iadd__ 
    1188 
    1189    def pop(fn): 
    1190        def pop(self, index=-1): 
    1191            __before_pop(self) 
    1192            item = fn(self, index) 
    1193            __del(self, item, None, index) 
    1194            return item 
    1195 
    1196        _tidy(pop) 
    1197        return pop 
    1198 
    1199    def clear(fn): 
    1200        def clear(self, index=-1): 
    1201            for item in self: 
    1202                __del(self, item, None, index) 
    1203            fn(self) 
    1204 
    1205        _tidy(clear) 
    1206        return clear 
    1207 
    1208    # __imul__ : not wrapping this.  all members of the collection are already 
    1209    # present, so no need to fire appends... wrapping it with an explicit 
    1210    # decorator is still possible, so events on *= can be had if they're 
    1211    # desired.  hard to imagine a use case for __imul__, though. 
    1212 
    1213    l = locals().copy() 
    1214    l.pop("_tidy") 
    1215    return l 
    1216 
    1217 
    1218def _dict_decorators() -> Dict[str, Callable[[_FN], _FN]]: 
    1219    """Tailored instrumentation wrappers for any dict-like mapping class.""" 
    1220 
    1221    def _tidy(fn): 
    1222        fn._sa_instrumented = True 
    1223        fn.__doc__ = getattr(dict, fn.__name__).__doc__ 
    1224 
    1225    def __setitem__(fn): 
    1226        def __setitem__(self, key, value, _sa_initiator=None): 
    1227            if key in self: 
    1228                __del(self, self[key], _sa_initiator, key) 
    1229            value = __set(self, value, _sa_initiator, key) 
    1230            fn(self, key, value) 
    1231 
    1232        _tidy(__setitem__) 
    1233        return __setitem__ 
    1234 
    1235    def __delitem__(fn): 
    1236        def __delitem__(self, key, _sa_initiator=None): 
    1237            if key in self: 
    1238                __del(self, self[key], _sa_initiator, key) 
    1239            fn(self, key) 
    1240 
    1241        _tidy(__delitem__) 
    1242        return __delitem__ 
    1243 
    1244    def clear(fn): 
    1245        def clear(self): 
    1246            for key in self: 
    1247                __del(self, self[key], None, key) 
    1248            fn(self) 
    1249 
    1250        _tidy(clear) 
    1251        return clear 
    1252 
    1253    def pop(fn): 
    1254        def pop(self, key, default=NO_ARG): 
    1255            __before_pop(self) 
    1256            _to_del = key in self 
    1257            if default is NO_ARG: 
    1258                item = fn(self, key) 
    1259            else: 
    1260                item = fn(self, key, default) 
    1261            if _to_del: 
    1262                __del(self, item, None, key) 
    1263            return item 
    1264 
    1265        _tidy(pop) 
    1266        return pop 
    1267 
    1268    def popitem(fn): 
    1269        def popitem(self): 
    1270            __before_pop(self) 
    1271            item = fn(self) 
    1272            __del(self, item[1], None, 1) 
    1273            return item 
    1274 
    1275        _tidy(popitem) 
    1276        return popitem 
    1277 
    1278    def setdefault(fn): 
    1279        def setdefault(self, key, default=None): 
    1280            if key not in self: 
    1281                self.__setitem__(key, default) 
    1282                return default 
    1283            else: 
    1284                value = self.__getitem__(key) 
    1285                if value is default: 
    1286                    __set_wo_mutation(self, value, None) 
    1287 
    1288                return value 
    1289 
    1290        _tidy(setdefault) 
    1291        return setdefault 
    1292 
    1293    def update(fn): 
    1294        def update(self, __other=NO_ARG, **kw): 
    1295            if __other is not NO_ARG: 
    1296                if hasattr(__other, "keys"): 
    1297                    for key in list(__other): 
    1298                        if key not in self or self[key] is not __other[key]: 
    1299                            self[key] = __other[key] 
    1300                        else: 
    1301                            __set_wo_mutation(self, __other[key], None) 
    1302                else: 
    1303                    for key, value in __other: 
    1304                        if key not in self or self[key] is not value: 
    1305                            self[key] = value 
    1306                        else: 
    1307                            __set_wo_mutation(self, value, None) 
    1308            for key in kw: 
    1309                if key not in self or self[key] is not kw[key]: 
    1310                    self[key] = kw[key] 
    1311                else: 
    1312                    __set_wo_mutation(self, kw[key], None) 
    1313 
    1314        _tidy(update) 
    1315        return update 
    1316 
    1317    l = locals().copy() 
    1318    l.pop("_tidy") 
    1319    return l 
    1320 
    1321 
    1322_set_binop_bases = (set, frozenset) 
    1323 
    1324 
    1325def _set_binops_check_strict(self: Any, obj: Any) -> bool: 
    1326    """Allow only set, frozenset and self.__class__-derived 
    1327    objects in binops.""" 
    1328    return isinstance(obj, _set_binop_bases + (self.__class__,)) 
    1329 
    1330 
    1331def _set_decorators() -> Dict[str, Callable[[_FN], _FN]]: 
    1332    """Tailored instrumentation wrappers for any set-like class.""" 
    1333 
    1334    def _tidy(fn): 
    1335        fn._sa_instrumented = True 
    1336        fn.__doc__ = getattr(set, fn.__name__).__doc__ 
    1337 
    1338    def add(fn): 
    1339        def add(self, value, _sa_initiator=None): 
    1340            if value not in self: 
    1341                value = __set(self, value, _sa_initiator, NO_KEY) 
    1342            else: 
    1343                __set_wo_mutation(self, value, _sa_initiator) 
    1344            # testlib.pragma exempt:__hash__ 
    1345            fn(self, value) 
    1346 
    1347        _tidy(add) 
    1348        return add 
    1349 
    1350    def discard(fn): 
    1351        def discard(self, value, _sa_initiator=None): 
    1352            # testlib.pragma exempt:__hash__ 
    1353            if value in self: 
    1354                __del(self, value, _sa_initiator, NO_KEY) 
    1355                # testlib.pragma exempt:__hash__ 
    1356            fn(self, value) 
    1357 
    1358        _tidy(discard) 
    1359        return discard 
    1360 
    1361    def remove(fn): 
    1362        def remove(self, value, _sa_initiator=None): 
    1363            # testlib.pragma exempt:__hash__ 
    1364            if value in self: 
    1365                __del(self, value, _sa_initiator, NO_KEY) 
    1366            # testlib.pragma exempt:__hash__ 
    1367            fn(self, value) 
    1368 
    1369        _tidy(remove) 
    1370        return remove 
    1371 
    1372    def pop(fn): 
    1373        def pop(self): 
    1374            __before_pop(self) 
    1375            item = fn(self) 
    1376            # for set in particular, we have no way to access the item 
    1377            # that will be popped before pop is called. 
    1378            __del(self, item, None, NO_KEY) 
    1379            return item 
    1380 
    1381        _tidy(pop) 
    1382        return pop 
    1383 
    1384    def clear(fn): 
    1385        def clear(self): 
    1386            for item in list(self): 
    1387                self.remove(item) 
    1388 
    1389        _tidy(clear) 
    1390        return clear 
    1391 
    1392    def update(fn): 
    1393        def update(self, value): 
    1394            for item in value: 
    1395                self.add(item) 
    1396 
    1397        _tidy(update) 
    1398        return update 
    1399 
    1400    def __ior__(fn): 
    1401        def __ior__(self, value): 
    1402            if not _set_binops_check_strict(self, value): 
    1403                return NotImplemented 
    1404            for item in value: 
    1405                self.add(item) 
    1406            return self 
    1407 
    1408        _tidy(__ior__) 
    1409        return __ior__ 
    1410 
    1411    def difference_update(fn): 
    1412        def difference_update(self, value): 
    1413            for item in value: 
    1414                self.discard(item) 
    1415 
    1416        _tidy(difference_update) 
    1417        return difference_update 
    1418 
    1419    def __isub__(fn): 
    1420        def __isub__(self, value): 
    1421            if not _set_binops_check_strict(self, value): 
    1422                return NotImplemented 
    1423            for item in value: 
    1424                self.discard(item) 
    1425            return self 
    1426 
    1427        _tidy(__isub__) 
    1428        return __isub__ 
    1429 
    1430    def intersection_update(fn): 
    1431        def intersection_update(self, other): 
    1432            want, have = self.intersection(other), set(self) 
    1433            remove, add = have - want, want - have 
    1434 
    1435            for item in remove: 
    1436                self.remove(item) 
    1437            for item in add: 
    1438                self.add(item) 
    1439 
    1440        _tidy(intersection_update) 
    1441        return intersection_update 
    1442 
    1443    def __iand__(fn): 
    1444        def __iand__(self, other): 
    1445            if not _set_binops_check_strict(self, other): 
    1446                return NotImplemented 
    1447            want, have = self.intersection(other), set(self) 
    1448            remove, add = have - want, want - have 
    1449 
    1450            for item in remove: 
    1451                self.remove(item) 
    1452            for item in add: 
    1453                self.add(item) 
    1454            return self 
    1455 
    1456        _tidy(__iand__) 
    1457        return __iand__ 
    1458 
    1459    def symmetric_difference_update(fn): 
    1460        def symmetric_difference_update(self, other): 
    1461            want, have = self.symmetric_difference(other), set(self) 
    1462            remove, add = have - want, want - have 
    1463 
    1464            for item in remove: 
    1465                self.remove(item) 
    1466            for item in add: 
    1467                self.add(item) 
    1468 
    1469        _tidy(symmetric_difference_update) 
    1470        return symmetric_difference_update 
    1471 
    1472    def __ixor__(fn): 
    1473        def __ixor__(self, other): 
    1474            if not _set_binops_check_strict(self, other): 
    1475                return NotImplemented 
    1476            want, have = self.symmetric_difference(other), set(self) 
    1477            remove, add = have - want, want - have 
    1478 
    1479            for item in remove: 
    1480                self.remove(item) 
    1481            for item in add: 
    1482                self.add(item) 
    1483            return self 
    1484 
    1485        _tidy(__ixor__) 
    1486        return __ixor__ 
    1487 
    1488    l = locals().copy() 
    1489    l.pop("_tidy") 
    1490    return l 
    1491 
    1492 
    1493class InstrumentedList(List[_T]): 
    1494    """An instrumented version of the built-in list.""" 
    1495 
    1496 
    1497class InstrumentedSet(Set[_T]): 
    1498    """An instrumented version of the built-in set.""" 
    1499 
    1500 
    1501class InstrumentedDict(Dict[_KT, _VT]): 
    1502    """An instrumented version of the built-in dict.""" 
    1503 
    1504 
    1505__canned_instrumentation = cast( 
    1506    util.immutabledict[Any, _CollectionFactoryType], 
    1507    util.immutabledict( 
    1508        { 
    1509            list: InstrumentedList, 
    1510            set: InstrumentedSet, 
    1511            dict: InstrumentedDict, 
    1512        } 
    1513    ), 
    1514) 
    1515 
    1516__interfaces: util.immutabledict[ 
    1517    Any, 
    1518    Tuple[ 
    1519        Dict[str, str], 
    1520        Dict[str, Callable[..., Any]], 
    1521    ], 
    1522] = util.immutabledict( 
    1523    { 
    1524        list: ( 
    1525            { 
    1526                "appender": "append", 
    1527                "remover": "remove", 
    1528                "iterator": "__iter__", 
    1529            }, 
    1530            _list_decorators(), 
    1531        ), 
    1532        set: ( 
    1533            {"appender": "add", "remover": "remove", "iterator": "__iter__"}, 
    1534            _set_decorators(), 
    1535        ), 
    1536        # decorators are required for dicts and object collections. 
    1537        dict: ({"iterator": "values"}, _dict_decorators()), 
    1538    } 
    1539) 
    1540 
    1541 
    1542def __go(lcls): 
    1543    global keyfunc_mapping, mapped_collection 
    1544    global column_keyed_dict, column_mapped_collection 
    1545    global MappedCollection, KeyFuncDict 
    1546    global attribute_keyed_dict, attribute_mapped_collection 
    1547 
    1548    from .mapped_collection import keyfunc_mapping 
    1549    from .mapped_collection import column_keyed_dict 
    1550    from .mapped_collection import attribute_keyed_dict 
    1551    from .mapped_collection import KeyFuncDict 
    1552 
    1553    from .mapped_collection import mapped_collection 
    1554    from .mapped_collection import column_mapped_collection 
    1555    from .mapped_collection import attribute_mapped_collection 
    1556    from .mapped_collection import MappedCollection 
    1557 
    1558    # ensure instrumentation is associated with 
    1559    # these built-in classes; if a user-defined class 
    1560    # subclasses these and uses @internally_instrumented, 
    1561    # the superclass is otherwise not instrumented. 
    1562    # see [ticket:2406]. 
    1563    _instrument_class(InstrumentedList) 
    1564    _instrument_class(InstrumentedSet) 
    1565    _instrument_class(KeyFuncDict) 
    1566 
    1567 
    1568__go(locals())