1# event/base.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"""Base implementation classes. 
    9 
    10The public-facing ``Events`` serves as the base class for an event interface; 
    11its public attributes represent different kinds of events.   These attributes 
    12are mirrored onto a ``_Dispatch`` class, which serves as a container for 
    13collections of listener functions.   These collections are represented both 
    14at the class level of a particular ``_Dispatch`` class as well as within 
    15instances of ``_Dispatch``. 
    16 
    17""" 
    18from __future__ import absolute_import 
    19 
    20import weakref 
    21 
    22from .attr import _ClsLevelDispatch 
    23from .attr import _EmptyListener 
    24from .attr import _JoinedListener 
    25from .. import util 
    26 
    27 
    28_registrars = util.defaultdict(list) 
    29 
    30 
    31def _is_event_name(name): 
    32    # _sa_event prefix is special to support internal-only event names. 
    33    # most event names are just plain method names that aren't 
    34    # underscored. 
    35 
    36    return ( 
    37        not name.startswith("_") and name != "dispatch" 
    38    ) or name.startswith("_sa_event") 
    39 
    40 
    41class _UnpickleDispatch(object): 
    42    """Serializable callable that re-generates an instance of 
    43    :class:`_Dispatch` given a particular :class:`.Events` subclass. 
    44 
    45    """ 
    46 
    47    def __call__(self, _instance_cls): 
    48        for cls in _instance_cls.__mro__: 
    49            if "dispatch" in cls.__dict__: 
    50                return cls.__dict__["dispatch"].dispatch._for_class( 
    51                    _instance_cls 
    52                ) 
    53        else: 
    54            raise AttributeError("No class with a 'dispatch' member present.") 
    55 
    56 
    57class _Dispatch(object): 
    58    """Mirror the event listening definitions of an Events class with 
    59    listener collections. 
    60 
    61    Classes which define a "dispatch" member will return a 
    62    non-instantiated :class:`._Dispatch` subclass when the member 
    63    is accessed at the class level.  When the "dispatch" member is 
    64    accessed at the instance level of its owner, an instance 
    65    of the :class:`._Dispatch` class is returned. 
    66 
    67    A :class:`._Dispatch` class is generated for each :class:`.Events` 
    68    class defined, by the :func:`._create_dispatcher_class` function. 
    69    The original :class:`.Events` classes remain untouched. 
    70    This decouples the construction of :class:`.Events` subclasses from 
    71    the implementation used by the event internals, and allows 
    72    inspecting tools like Sphinx to work in an unsurprising 
    73    way against the public API. 
    74 
    75    """ 
    76 
    77    # In one ORM edge case, an attribute is added to _Dispatch, 
    78    # so __dict__ is used in just that case and potentially others. 
    79    __slots__ = "_parent", "_instance_cls", "__dict__", "_empty_listeners" 
    80 
    81    _empty_listener_reg = weakref.WeakKeyDictionary() 
    82 
    83    def __init__(self, parent, instance_cls=None): 
    84        self._parent = parent 
    85        self._instance_cls = instance_cls 
    86 
    87        if instance_cls: 
    88            try: 
    89                self._empty_listeners = self._empty_listener_reg[instance_cls] 
    90            except KeyError: 
    91                self._empty_listeners = self._empty_listener_reg[ 
    92                    instance_cls 
    93                ] = { 
    94                    ls.name: _EmptyListener(ls, instance_cls) 
    95                    for ls in parent._event_descriptors 
    96                } 
    97        else: 
    98            self._empty_listeners = {} 
    99 
    100    def __getattr__(self, name): 
    101        # Assign EmptyListeners as attributes on demand 
    102        # to reduce startup time for new dispatch objects. 
    103        try: 
    104            ls = self._empty_listeners[name] 
    105        except KeyError: 
    106            raise AttributeError(name) 
    107        else: 
    108            setattr(self, ls.name, ls) 
    109            return ls 
    110 
    111    @property 
    112    def _event_descriptors(self): 
    113        for k in self._event_names: 
    114            # Yield _ClsLevelDispatch related 
    115            # to relevant event name. 
    116            yield getattr(self, k) 
    117 
    118    @property 
    119    def _listen(self): 
    120        return self._events._listen 
    121 
    122    def _for_class(self, instance_cls): 
    123        return self.__class__(self, instance_cls) 
    124 
    125    def _for_instance(self, instance): 
    126        instance_cls = instance.__class__ 
    127        return self._for_class(instance_cls) 
    128 
    129    def _join(self, other): 
    130        """Create a 'join' of this :class:`._Dispatch` and another. 
    131 
    132        This new dispatcher will dispatch events to both 
    133        :class:`._Dispatch` objects. 
    134 
    135        """ 
    136        if "_joined_dispatch_cls" not in self.__class__.__dict__: 
    137            cls = type( 
    138                "Joined%s" % self.__class__.__name__, 
    139                (_JoinedDispatcher,), 
    140                {"__slots__": self._event_names}, 
    141            ) 
    142 
    143            self.__class__._joined_dispatch_cls = cls 
    144        return self._joined_dispatch_cls(self, other) 
    145 
    146    def __reduce__(self): 
    147        return _UnpickleDispatch(), (self._instance_cls,) 
    148 
    149    def _update(self, other, only_propagate=True): 
    150        """Populate from the listeners in another :class:`_Dispatch` 
    151        object.""" 
    152        for ls in other._event_descriptors: 
    153            if isinstance(ls, _EmptyListener): 
    154                continue 
    155            getattr(self, ls.name).for_modify(self)._update( 
    156                ls, only_propagate=only_propagate 
    157            ) 
    158 
    159    def _clear(self): 
    160        for ls in self._event_descriptors: 
    161            ls.for_modify(self).clear() 
    162 
    163 
    164class _EventMeta(type): 
    165    """Intercept new Event subclasses and create 
    166    associated _Dispatch classes.""" 
    167 
    168    def __init__(cls, classname, bases, dict_): 
    169        _create_dispatcher_class(cls, classname, bases, dict_) 
    170        type.__init__(cls, classname, bases, dict_) 
    171 
    172 
    173def _create_dispatcher_class(cls, classname, bases, dict_): 
    174    """Create a :class:`._Dispatch` class corresponding to an 
    175    :class:`.Events` class.""" 
    176 
    177    # there's all kinds of ways to do this, 
    178    # i.e. make a Dispatch class that shares the '_listen' method 
    179    # of the Event class, this is the straight monkeypatch. 
    180    if hasattr(cls, "dispatch"): 
    181        dispatch_base = cls.dispatch.__class__ 
    182    else: 
    183        dispatch_base = _Dispatch 
    184 
    185    event_names = [k for k in dict_ if _is_event_name(k)] 
    186    dispatch_cls = type( 
    187        "%sDispatch" % classname, (dispatch_base,), {"__slots__": event_names} 
    188    ) 
    189 
    190    dispatch_cls._event_names = event_names 
    191 
    192    dispatch_inst = cls._set_dispatch(cls, dispatch_cls) 
    193    for k in dispatch_cls._event_names: 
    194        setattr(dispatch_inst, k, _ClsLevelDispatch(cls, dict_[k])) 
    195        _registrars[k].append(cls) 
    196 
    197    for super_ in dispatch_cls.__bases__: 
    198        if issubclass(super_, _Dispatch) and super_ is not _Dispatch: 
    199            for ls in super_._events.dispatch._event_descriptors: 
    200                setattr(dispatch_inst, ls.name, ls) 
    201                dispatch_cls._event_names.append(ls.name) 
    202 
    203    if getattr(cls, "_dispatch_target", None): 
    204        cls._dispatch_target.dispatch = dispatcher(cls) 
    205 
    206 
    207def _remove_dispatcher(cls): 
    208    for k in cls.dispatch._event_names: 
    209        _registrars[k].remove(cls) 
    210        if not _registrars[k]: 
    211            del _registrars[k] 
    212 
    213 
    214class Events(util.with_metaclass(_EventMeta, object)): 
    215    """Define event listening functions for a particular target type.""" 
    216 
    217    @staticmethod 
    218    def _set_dispatch(cls, dispatch_cls): 
    219        # This allows an Events subclass to define additional utility 
    220        # methods made available to the target via 
    221        # "self.dispatch._events.<utilitymethod>" 
    222        # @staticmethod to allow easy "super" calls while in a metaclass 
    223        # constructor. 
    224        cls.dispatch = dispatch_cls(None) 
    225        dispatch_cls._events = cls 
    226        return cls.dispatch 
    227 
    228    @classmethod 
    229    def _accept_with(cls, target): 
    230        def dispatch_is(*types): 
    231            return all(isinstance(target.dispatch, t) for t in types) 
    232 
    233        def dispatch_parent_is(t): 
    234            return isinstance(target.dispatch.parent, t) 
    235 
    236        # Mapper, ClassManager, Session override this to 
    237        # also accept classes, scoped_sessions, sessionmakers, etc. 
    238        if hasattr(target, "dispatch"): 
    239            if ( 
    240                dispatch_is(cls.dispatch.__class__) 
    241                or dispatch_is(type, cls.dispatch.__class__) 
    242                or ( 
    243                    dispatch_is(_JoinedDispatcher) 
    244                    and dispatch_parent_is(cls.dispatch.__class__) 
    245                ) 
    246            ): 
    247                return target 
    248 
    249    @classmethod 
    250    def _listen(cls, event_key, propagate=False, insert=False, named=False): 
    251        event_key.base_listen(propagate=propagate, insert=insert, named=named) 
    252 
    253    @classmethod 
    254    def _remove(cls, event_key): 
    255        event_key.remove() 
    256 
    257    @classmethod 
    258    def _clear(cls): 
    259        cls.dispatch._clear() 
    260 
    261 
    262class _JoinedDispatcher(object): 
    263    """Represent a connection between two _Dispatch objects.""" 
    264 
    265    __slots__ = "local", "parent", "_instance_cls" 
    266 
    267    def __init__(self, local, parent): 
    268        self.local = local 
    269        self.parent = parent 
    270        self._instance_cls = self.local._instance_cls 
    271 
    272    def __getattr__(self, name): 
    273        # Assign _JoinedListeners as attributes on demand 
    274        # to reduce startup time for new dispatch objects. 
    275        ls = getattr(self.local, name) 
    276        jl = _JoinedListener(self.parent, ls.name, ls) 
    277        setattr(self, ls.name, jl) 
    278        return jl 
    279 
    280    @property 
    281    def _listen(self): 
    282        return self.parent._listen 
    283 
    284    @property 
    285    def _events(self): 
    286        return self.parent._events 
    287 
    288 
    289class dispatcher(object): 
    290    """Descriptor used by target classes to 
    291    deliver the _Dispatch class at the class level 
    292    and produce new _Dispatch instances for target 
    293    instances. 
    294 
    295    """ 
    296 
    297    def __init__(self, events): 
    298        self.dispatch = events.dispatch 
    299        self.events = events 
    300 
    301    def __get__(self, obj, cls): 
    302        if obj is None: 
    303            return self.dispatch 
    304        obj.__dict__["dispatch"] = disp = self.dispatch._for_instance(obj) 
    305        return disp