1# event/attr.py 
    2# Copyright (C) 2005-2021 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: http://www.opensource.org/licenses/mit-license.php 
    7 
    8"""Attribute implementation for _Dispatch classes. 
    9 
    10The various listener targets for a particular event class are represented 
    11as attributes, which refer to collections of listeners to be fired off. 
    12These collections can exist at the class level as well as at the instance 
    13level.  An event is fired off using code like this:: 
    14 
    15    some_object.dispatch.first_connect(arg1, arg2) 
    16 
    17Above, ``some_object.dispatch`` would be an instance of ``_Dispatch`` and 
    18``first_connect`` is typically an instance of ``_ListenerCollection`` 
    19if event listeners are present, or ``_EmptyListener`` if none are present. 
    20 
    21The attribute mechanics here spend effort trying to ensure listener functions 
    22are available with a minimum of function call overhead, that unnecessary 
    23objects aren't created (i.e. many empty per-instance listener collections), 
    24as well as that everything is garbage collectable when owning references are 
    25lost.  Other features such as "propagation" of listener functions across 
    26many ``_Dispatch`` instances, "joining" of multiple ``_Dispatch`` instances, 
    27as well as support for subclass propagation (e.g. events assigned to 
    28``Pool`` vs. ``QueuePool``) are all implemented here. 
    29 
    30""" 
    31 
    32from __future__ import absolute_import 
    33from __future__ import with_statement 
    34 
    35import collections 
    36from itertools import chain 
    37import weakref 
    38 
    39from . import legacy 
    40from . import registry 
    41from .. import exc 
    42from .. import util 
    43from ..util import threading 
    44 
    45 
    46class RefCollection(util.MemoizedSlots): 
    47    __slots__ = ("ref",) 
    48 
    49    def _memoized_attr_ref(self): 
    50        return weakref.ref(self, registry._collection_gced) 
    51 
    52 
    53class _empty_collection(object): 
    54    def append(self, element): 
    55        pass 
    56 
    57    def extend(self, other): 
    58        pass 
    59 
    60    def remove(self, element): 
    61        pass 
    62 
    63    def __iter__(self): 
    64        return iter([]) 
    65 
    66    def clear(self): 
    67        pass 
    68 
    69 
    70class _ClsLevelDispatch(RefCollection): 
    71    """Class-level events on :class:`._Dispatch` classes.""" 
    72 
    73    __slots__ = ( 
    74        "name", 
    75        "arg_names", 
    76        "has_kw", 
    77        "legacy_signatures", 
    78        "_clslevel", 
    79        "__weakref__", 
    80    ) 
    81 
    82    def __init__(self, parent_dispatch_cls, fn): 
    83        self.name = fn.__name__ 
    84        argspec = util.inspect_getfullargspec(fn) 
    85        self.arg_names = argspec.args[1:] 
    86        self.has_kw = bool(argspec.varkw) 
    87        self.legacy_signatures = list( 
    88            reversed( 
    89                sorted( 
    90                    getattr(fn, "_legacy_signatures", []), key=lambda s: s[0] 
    91                ) 
    92            ) 
    93        ) 
    94        fn.__doc__ = legacy._augment_fn_docs(self, parent_dispatch_cls, fn) 
    95 
    96        self._clslevel = weakref.WeakKeyDictionary() 
    97 
    98    def _adjust_fn_spec(self, fn, named): 
    99        if named: 
    100            fn = self._wrap_fn_for_kw(fn) 
    101        if self.legacy_signatures: 
    102            try: 
    103                argspec = util.get_callable_argspec(fn, no_self=True) 
    104            except TypeError: 
    105                pass 
    106            else: 
    107                fn = legacy._wrap_fn_for_legacy(self, fn, argspec) 
    108        return fn 
    109 
    110    def _wrap_fn_for_kw(self, fn): 
    111        def wrap_kw(*args, **kw): 
    112            argdict = dict(zip(self.arg_names, args)) 
    113            argdict.update(kw) 
    114            return fn(**argdict) 
    115 
    116        return wrap_kw 
    117 
    118    def insert(self, event_key, propagate): 
    119        target = event_key.dispatch_target 
    120        assert isinstance( 
    121            target, type 
    122        ), "Class-level Event targets must be classes." 
    123        if not getattr(target, "_sa_propagate_class_events", True): 
    124            raise exc.InvalidRequestError( 
    125                "Can't assign an event directly to the %s class" % target 
    126            ) 
    127        stack = [target] 
    128        while stack: 
    129            cls = stack.pop(0) 
    130            stack.extend(cls.__subclasses__()) 
    131            if cls is not target and cls not in self._clslevel: 
    132                self.update_subclass(cls) 
    133            else: 
    134                if cls not in self._clslevel: 
    135                    self._assign_cls_collection(cls) 
    136                self._clslevel[cls].appendleft(event_key._listen_fn) 
    137        registry._stored_in_collection(event_key, self) 
    138 
    139    def append(self, event_key, propagate): 
    140        target = event_key.dispatch_target 
    141        assert isinstance( 
    142            target, type 
    143        ), "Class-level Event targets must be classes." 
    144        if not getattr(target, "_sa_propagate_class_events", True): 
    145            raise exc.InvalidRequestError( 
    146                "Can't assign an event directly to the %s class" % target 
    147            ) 
    148        stack = [target] 
    149        while stack: 
    150            cls = stack.pop(0) 
    151            stack.extend(cls.__subclasses__()) 
    152            if cls is not target and cls not in self._clslevel: 
    153                self.update_subclass(cls) 
    154            else: 
    155                if cls not in self._clslevel: 
    156                    self._assign_cls_collection(cls) 
    157                self._clslevel[cls].append(event_key._listen_fn) 
    158        registry._stored_in_collection(event_key, self) 
    159 
    160    def _assign_cls_collection(self, target): 
    161        if getattr(target, "_sa_propagate_class_events", True): 
    162            self._clslevel[target] = collections.deque() 
    163        else: 
    164            self._clslevel[target] = _empty_collection() 
    165 
    166    def update_subclass(self, target): 
    167        if target not in self._clslevel: 
    168            self._assign_cls_collection(target) 
    169        clslevel = self._clslevel[target] 
    170        for cls in target.__mro__[1:]: 
    171            if cls in self._clslevel: 
    172                clslevel.extend( 
    173                    [fn for fn in self._clslevel[cls] if fn not in clslevel] 
    174                ) 
    175 
    176    def remove(self, event_key): 
    177        target = event_key.dispatch_target 
    178        stack = [target] 
    179        while stack: 
    180            cls = stack.pop(0) 
    181            stack.extend(cls.__subclasses__()) 
    182            if cls in self._clslevel: 
    183                self._clslevel[cls].remove(event_key._listen_fn) 
    184        registry._removed_from_collection(event_key, self) 
    185 
    186    def clear(self): 
    187        """Clear all class level listeners""" 
    188 
    189        to_clear = set() 
    190        for dispatcher in self._clslevel.values(): 
    191            to_clear.update(dispatcher) 
    192            dispatcher.clear() 
    193        registry._clear(self, to_clear) 
    194 
    195    def for_modify(self, obj): 
    196        """Return an event collection which can be modified. 
    197 
    198        For _ClsLevelDispatch at the class level of 
    199        a dispatcher, this returns self. 
    200 
    201        """ 
    202        return self 
    203 
    204 
    205class _InstanceLevelDispatch(RefCollection): 
    206    __slots__ = () 
    207 
    208    def _adjust_fn_spec(self, fn, named): 
    209        return self.parent._adjust_fn_spec(fn, named) 
    210 
    211 
    212class _EmptyListener(_InstanceLevelDispatch): 
    213    """Serves as a proxy interface to the events 
    214    served by a _ClsLevelDispatch, when there are no 
    215    instance-level events present. 
    216 
    217    Is replaced by _ListenerCollection when instance-level 
    218    events are added. 
    219 
    220    """ 
    221 
    222    propagate = frozenset() 
    223    listeners = () 
    224 
    225    __slots__ = "parent", "parent_listeners", "name" 
    226 
    227    def __init__(self, parent, target_cls): 
    228        if target_cls not in parent._clslevel: 
    229            parent.update_subclass(target_cls) 
    230        self.parent = parent  # _ClsLevelDispatch 
    231        self.parent_listeners = parent._clslevel[target_cls] 
    232        self.name = parent.name 
    233 
    234    def for_modify(self, obj): 
    235        """Return an event collection which can be modified. 
    236 
    237        For _EmptyListener at the instance level of 
    238        a dispatcher, this generates a new 
    239        _ListenerCollection, applies it to the instance, 
    240        and returns it. 
    241 
    242        """ 
    243        result = _ListenerCollection(self.parent, obj._instance_cls) 
    244        if getattr(obj, self.name) is self: 
    245            setattr(obj, self.name, result) 
    246        else: 
    247            assert isinstance(getattr(obj, self.name), _JoinedListener) 
    248        return result 
    249 
    250    def _needs_modify(self, *args, **kw): 
    251        raise NotImplementedError("need to call for_modify()") 
    252 
    253    exec_once = ( 
    254        exec_once_unless_exception 
    255    ) = insert = append = remove = clear = _needs_modify 
    256 
    257    def __call__(self, *args, **kw): 
    258        """Execute this event.""" 
    259 
    260        for fn in self.parent_listeners: 
    261            fn(*args, **kw) 
    262 
    263    def __len__(self): 
    264        return len(self.parent_listeners) 
    265 
    266    def __iter__(self): 
    267        return iter(self.parent_listeners) 
    268 
    269    def __bool__(self): 
    270        return bool(self.parent_listeners) 
    271 
    272    __nonzero__ = __bool__ 
    273 
    274 
    275class _CompoundListener(_InstanceLevelDispatch): 
    276    __slots__ = "_exec_once_mutex", "_exec_once" 
    277 
    278    def _memoized_attr__exec_once_mutex(self): 
    279        return threading.Lock() 
    280 
    281    def _exec_once_impl(self, retry_on_exception, *args, **kw): 
    282        with self._exec_once_mutex: 
    283            if not self._exec_once: 
    284                try: 
    285                    self(*args, **kw) 
    286                    exception = False 
    287                except: 
    288                    exception = True 
    289                    raise 
    290                finally: 
    291                    if not exception or not retry_on_exception: 
    292                        self._exec_once = True 
    293 
    294    def exec_once(self, *args, **kw): 
    295        """Execute this event, but only if it has not been 
    296        executed already for this collection.""" 
    297 
    298        if not self._exec_once: 
    299            self._exec_once_impl(False, *args, **kw) 
    300 
    301    def exec_once_unless_exception(self, *args, **kw): 
    302        """Execute this event, but only if it has not been 
    303        executed already for this collection, or was called 
    304        by a previous exec_once_unless_exception call and 
    305        raised an exception. 
    306 
    307        If exec_once was already called, then this method will never run 
    308        the callable regardless of whether it raised or not. 
    309 
    310        .. versionadded:: 1.3.8 
    311 
    312        """ 
    313        if not self._exec_once: 
    314            self._exec_once_impl(True, *args, **kw) 
    315 
    316    def __call__(self, *args, **kw): 
    317        """Execute this event.""" 
    318 
    319        for fn in self.parent_listeners: 
    320            fn(*args, **kw) 
    321        for fn in self.listeners: 
    322            fn(*args, **kw) 
    323 
    324    def __len__(self): 
    325        return len(self.parent_listeners) + len(self.listeners) 
    326 
    327    def __iter__(self): 
    328        return chain(self.parent_listeners, self.listeners) 
    329 
    330    def __bool__(self): 
    331        return bool(self.listeners or self.parent_listeners) 
    332 
    333    __nonzero__ = __bool__ 
    334 
    335 
    336class _ListenerCollection(_CompoundListener): 
    337    """Instance-level attributes on instances of :class:`._Dispatch`. 
    338 
    339    Represents a collection of listeners. 
    340 
    341    As of 0.7.9, _ListenerCollection is only first 
    342    created via the _EmptyListener.for_modify() method. 
    343 
    344    """ 
    345 
    346    __slots__ = ( 
    347        "parent_listeners", 
    348        "parent", 
    349        "name", 
    350        "listeners", 
    351        "propagate", 
    352        "__weakref__", 
    353    ) 
    354 
    355    def __init__(self, parent, target_cls): 
    356        if target_cls not in parent._clslevel: 
    357            parent.update_subclass(target_cls) 
    358        self._exec_once = False 
    359        self.parent_listeners = parent._clslevel[target_cls] 
    360        self.parent = parent 
    361        self.name = parent.name 
    362        self.listeners = collections.deque() 
    363        self.propagate = set() 
    364 
    365    def for_modify(self, obj): 
    366        """Return an event collection which can be modified. 
    367 
    368        For _ListenerCollection at the instance level of 
    369        a dispatcher, this returns self. 
    370 
    371        """ 
    372        return self 
    373 
    374    def _update(self, other, only_propagate=True): 
    375        """Populate from the listeners in another :class:`_Dispatch` 
    376        object.""" 
    377 
    378        existing_listeners = self.listeners 
    379        existing_listener_set = set(existing_listeners) 
    380        self.propagate.update(other.propagate) 
    381        other_listeners = [ 
    382            l 
    383            for l in other.listeners 
    384            if l not in existing_listener_set 
    385            and not only_propagate 
    386            or l in self.propagate 
    387        ] 
    388 
    389        existing_listeners.extend(other_listeners) 
    390 
    391        to_associate = other.propagate.union(other_listeners) 
    392        registry._stored_in_collection_multi(self, other, to_associate) 
    393 
    394    def insert(self, event_key, propagate): 
    395        if event_key.prepend_to_list(self, self.listeners): 
    396            if propagate: 
    397                self.propagate.add(event_key._listen_fn) 
    398 
    399    def append(self, event_key, propagate): 
    400        if event_key.append_to_list(self, self.listeners): 
    401            if propagate: 
    402                self.propagate.add(event_key._listen_fn) 
    403 
    404    def remove(self, event_key): 
    405        self.listeners.remove(event_key._listen_fn) 
    406        self.propagate.discard(event_key._listen_fn) 
    407        registry._removed_from_collection(event_key, self) 
    408 
    409    def clear(self): 
    410        registry._clear(self, self.listeners) 
    411        self.propagate.clear() 
    412        self.listeners.clear() 
    413 
    414 
    415class _JoinedListener(_CompoundListener): 
    416    __slots__ = "parent", "name", "local", "parent_listeners" 
    417 
    418    def __init__(self, parent, name, local): 
    419        self._exec_once = False 
    420        self.parent = parent 
    421        self.name = name 
    422        self.local = local 
    423        self.parent_listeners = self.local 
    424 
    425    @property 
    426    def listeners(self): 
    427        return getattr(self.parent, self.name) 
    428 
    429    def _adjust_fn_spec(self, fn, named): 
    430        return self.local._adjust_fn_spec(fn, named) 
    431 
    432    def for_modify(self, obj): 
    433        self.local = self.parent_listeners = self.local.for_modify(obj) 
    434        return self 
    435 
    436    def insert(self, event_key, propagate): 
    437        self.local.insert(event_key, propagate) 
    438 
    439    def append(self, event_key, propagate): 
    440        self.local.append(event_key, propagate) 
    441 
    442    def remove(self, event_key): 
    443        self.local.remove(event_key) 
    444 
    445    def clear(self): 
    446        raise NotImplementedError()