Update 2025-04-13_16:25:39

This commit is contained in:
root
2025-04-13 16:25:41 +02:00
commit 4c711360d3
2979 changed files with 666585 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# event/__init__.py
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
# the MIT License: https://www.opensource.org/licenses/mit-license.php
from __future__ import annotations
from .api import CANCEL as CANCEL
from .api import contains as contains
from .api import listen as listen
from .api import listens_for as listens_for
from .api import NO_RETVAL as NO_RETVAL
from .api import remove as remove
from .attr import _InstanceLevelDispatch as _InstanceLevelDispatch
from .attr import RefCollection as RefCollection
from .base import _Dispatch as _Dispatch
from .base import _DispatchCommon as _DispatchCommon
from .base import dispatcher as dispatcher
from .base import Events as Events
from .legacy import _legacy_signature as _legacy_signature
from .registry import _EventKey as _EventKey
from .registry import _ListenerFnType as _ListenerFnType
from .registry import EventTarget as EventTarget

View File

@ -0,0 +1,222 @@
# event/api.py
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
# the MIT License: https://www.opensource.org/licenses/mit-license.php
"""Public API functions for the event system.
"""
from __future__ import annotations
from typing import Any
from typing import Callable
from .base import _registrars
from .registry import _ET
from .registry import _EventKey
from .registry import _ListenerFnType
from .. import exc
from .. import util
CANCEL = util.symbol("CANCEL")
NO_RETVAL = util.symbol("NO_RETVAL")
def _event_key(
target: _ET, identifier: str, fn: _ListenerFnType
) -> _EventKey[_ET]:
for evt_cls in _registrars[identifier]:
tgt = evt_cls._accept_with(target, identifier)
if tgt is not None:
return _EventKey(target, identifier, fn, tgt)
else:
raise exc.InvalidRequestError(
"No such event '%s' for target '%s'" % (identifier, target)
)
def listen(
target: Any, identifier: str, fn: Callable[..., Any], *args: Any, **kw: Any
) -> None:
"""Register a listener function for the given target.
The :func:`.listen` function is part of the primary interface for the
SQLAlchemy event system, documented at :ref:`event_toplevel`.
e.g.::
from sqlalchemy import event
from sqlalchemy.schema import UniqueConstraint
def unique_constraint_name(const, table):
const.name = "uq_%s_%s" % (table.name, list(const.columns)[0].name)
event.listen(
UniqueConstraint, "after_parent_attach", unique_constraint_name
)
:param bool insert: The default behavior for event handlers is to append
the decorated user defined function to an internal list of registered
event listeners upon discovery. If a user registers a function with
``insert=True``, SQLAlchemy will insert (prepend) the function to the
internal list upon discovery. This feature is not typically used or
recommended by the SQLAlchemy maintainers, but is provided to ensure
certain user defined functions can run before others, such as when
:ref:`Changing the sql_mode in MySQL <mysql_sql_mode>`.
:param bool named: When using named argument passing, the names listed in
the function argument specification will be used as keys in the
dictionary.
See :ref:`event_named_argument_styles`.
:param bool once: Private/Internal API usage. Deprecated. This parameter
would provide that an event function would run only once per given
target. It does not however imply automatic de-registration of the
listener function; associating an arbitrarily high number of listeners
without explicitly removing them will cause memory to grow unbounded even
if ``once=True`` is specified.
:param bool propagate: The ``propagate`` kwarg is available when working
with ORM instrumentation and mapping events.
See :class:`_ormevent.MapperEvents` and
:meth:`_ormevent.MapperEvents.before_mapper_configured` for examples.
:param bool retval: This flag applies only to specific event listeners,
each of which includes documentation explaining when it should be used.
By default, no listener ever requires a return value.
However, some listeners do support special behaviors for return values,
and include in their documentation that the ``retval=True`` flag is
necessary for a return value to be processed.
Event listener suites that make use of :paramref:`_event.listen.retval`
include :class:`_events.ConnectionEvents` and
:class:`_ormevent.AttributeEvents`.
.. note::
The :func:`.listen` function cannot be called at the same time
that the target event is being run. This has implications
for thread safety, and also means an event cannot be added
from inside the listener function for itself. The list of
events to be run are present inside of a mutable collection
that can't be changed during iteration.
Event registration and removal is not intended to be a "high
velocity" operation; it is a configurational operation. For
systems that need to quickly associate and deassociate with
events at high scale, use a mutable structure that is handled
from inside of a single listener.
.. seealso::
:func:`.listens_for`
:func:`.remove`
"""
_event_key(target, identifier, fn).listen(*args, **kw)
def listens_for(
target: Any, identifier: str, *args: Any, **kw: Any
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""Decorate a function as a listener for the given target + identifier.
The :func:`.listens_for` decorator is part of the primary interface for the
SQLAlchemy event system, documented at :ref:`event_toplevel`.
This function generally shares the same kwargs as :func:`.listen`.
e.g.::
from sqlalchemy import event
from sqlalchemy.schema import UniqueConstraint
@event.listens_for(UniqueConstraint, "after_parent_attach")
def unique_constraint_name(const, table):
const.name = "uq_%s_%s" % (table.name, list(const.columns)[0].name)
A given function can also be invoked for only the first invocation
of the event using the ``once`` argument::
@event.listens_for(Mapper, "before_configure", once=True)
def on_config():
do_config()
.. warning:: The ``once`` argument does not imply automatic de-registration
of the listener function after it has been invoked a first time; a
listener entry will remain associated with the target object.
Associating an arbitrarily high number of listeners without explicitly
removing them will cause memory to grow unbounded even if ``once=True``
is specified.
.. seealso::
:func:`.listen` - general description of event listening
"""
def decorate(fn: Callable[..., Any]) -> Callable[..., Any]:
listen(target, identifier, fn, *args, **kw)
return fn
return decorate
def remove(target: Any, identifier: str, fn: Callable[..., Any]) -> None:
"""Remove an event listener.
The arguments here should match exactly those which were sent to
:func:`.listen`; all the event registration which proceeded as a result
of this call will be reverted by calling :func:`.remove` with the same
arguments.
e.g.::
# if a function was registered like this...
@event.listens_for(SomeMappedClass, "before_insert", propagate=True)
def my_listener_function(*arg):
pass
# ... it's removed like this
event.remove(SomeMappedClass, "before_insert", my_listener_function)
Above, the listener function associated with ``SomeMappedClass`` was also
propagated to subclasses of ``SomeMappedClass``; the :func:`.remove`
function will revert all of these operations.
.. note::
The :func:`.remove` function cannot be called at the same time
that the target event is being run. This has implications
for thread safety, and also means an event cannot be removed
from inside the listener function for itself. The list of
events to be run are present inside of a mutable collection
that can't be changed during iteration.
Event registration and removal is not intended to be a "high
velocity" operation; it is a configurational operation. For
systems that need to quickly associate and deassociate with
events at high scale, use a mutable structure that is handled
from inside of a single listener.
.. seealso::
:func:`.listen`
"""
_event_key(target, identifier, fn).remove()
def contains(target: Any, identifier: str, fn: Callable[..., Any]) -> bool:
"""Return True if the given target/ident/fn is set up to listen."""
return _event_key(target, identifier, fn).contains()

View File

@ -0,0 +1,655 @@
# event/attr.py
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
# the MIT License: https://www.opensource.org/licenses/mit-license.php
"""Attribute implementation for _Dispatch classes.
The various listener targets for a particular event class are represented
as attributes, which refer to collections of listeners to be fired off.
These collections can exist at the class level as well as at the instance
level. An event is fired off using code like this::
some_object.dispatch.first_connect(arg1, arg2)
Above, ``some_object.dispatch`` would be an instance of ``_Dispatch`` and
``first_connect`` is typically an instance of ``_ListenerCollection``
if event listeners are present, or ``_EmptyListener`` if none are present.
The attribute mechanics here spend effort trying to ensure listener functions
are available with a minimum of function call overhead, that unnecessary
objects aren't created (i.e. many empty per-instance listener collections),
as well as that everything is garbage collectable when owning references are
lost. Other features such as "propagation" of listener functions across
many ``_Dispatch`` instances, "joining" of multiple ``_Dispatch`` instances,
as well as support for subclass propagation (e.g. events assigned to
``Pool`` vs. ``QueuePool``) are all implemented here.
"""
from __future__ import annotations
import collections
from itertools import chain
import threading
from types import TracebackType
import typing
from typing import Any
from typing import cast
from typing import Collection
from typing import Deque
from typing import FrozenSet
from typing import Generic
from typing import Iterator
from typing import MutableMapping
from typing import MutableSequence
from typing import NoReturn
from typing import Optional
from typing import Sequence
from typing import Set
from typing import Tuple
from typing import Type
from typing import TypeVar
from typing import Union
import weakref
from . import legacy
from . import registry
from .registry import _ET
from .registry import _EventKey
from .registry import _ListenerFnType
from .. import exc
from .. import util
from ..util.concurrency import AsyncAdaptedLock
from ..util.typing import Protocol
_T = TypeVar("_T", bound=Any)
if typing.TYPE_CHECKING:
from .base import _Dispatch
from .base import _DispatchCommon
from .base import _HasEventsDispatch
class RefCollection(util.MemoizedSlots, Generic[_ET]):
__slots__ = ("ref",)
ref: weakref.ref[RefCollection[_ET]]
def _memoized_attr_ref(self) -> weakref.ref[RefCollection[_ET]]:
return weakref.ref(self, registry._collection_gced)
class _empty_collection(Collection[_T]):
def append(self, element: _T) -> None:
pass
def appendleft(self, element: _T) -> None:
pass
def extend(self, other: Sequence[_T]) -> None:
pass
def remove(self, element: _T) -> None:
pass
def __contains__(self, element: Any) -> bool:
return False
def __iter__(self) -> Iterator[_T]:
return iter([])
def clear(self) -> None:
pass
def __len__(self) -> int:
return 0
_ListenerFnSequenceType = Union[Deque[_T], _empty_collection[_T]]
class _ClsLevelDispatch(RefCollection[_ET]):
"""Class-level events on :class:`._Dispatch` classes."""
__slots__ = (
"clsname",
"name",
"arg_names",
"has_kw",
"legacy_signatures",
"_clslevel",
"__weakref__",
)
clsname: str
name: str
arg_names: Sequence[str]
has_kw: bool
legacy_signatures: MutableSequence[legacy._LegacySignatureType]
_clslevel: MutableMapping[
Type[_ET], _ListenerFnSequenceType[_ListenerFnType]
]
def __init__(
self,
parent_dispatch_cls: Type[_HasEventsDispatch[_ET]],
fn: _ListenerFnType,
):
self.name = fn.__name__
self.clsname = parent_dispatch_cls.__name__
argspec = util.inspect_getfullargspec(fn)
self.arg_names = argspec.args[1:]
self.has_kw = bool(argspec.varkw)
self.legacy_signatures = list(
reversed(
sorted(
getattr(fn, "_legacy_signatures", []), key=lambda s: s[0]
)
)
)
fn.__doc__ = legacy._augment_fn_docs(self, parent_dispatch_cls, fn)
self._clslevel = weakref.WeakKeyDictionary()
def _adjust_fn_spec(
self, fn: _ListenerFnType, named: bool
) -> _ListenerFnType:
if named:
fn = self._wrap_fn_for_kw(fn)
if self.legacy_signatures:
try:
argspec = util.get_callable_argspec(fn, no_self=True)
except TypeError:
pass
else:
fn = legacy._wrap_fn_for_legacy(self, fn, argspec)
return fn
def _wrap_fn_for_kw(self, fn: _ListenerFnType) -> _ListenerFnType:
def wrap_kw(*args: Any, **kw: Any) -> Any:
argdict = dict(zip(self.arg_names, args))
argdict.update(kw)
return fn(**argdict)
return wrap_kw
def _do_insert_or_append(
self, event_key: _EventKey[_ET], is_append: bool
) -> None:
target = event_key.dispatch_target
assert isinstance(
target, type
), "Class-level Event targets must be classes."
if not getattr(target, "_sa_propagate_class_events", True):
raise exc.InvalidRequestError(
f"Can't assign an event directly to the {target} class"
)
cls: Type[_ET]
for cls in util.walk_subclasses(target):
if cls is not target and cls not in self._clslevel:
self.update_subclass(cls)
else:
if cls not in self._clslevel:
self.update_subclass(cls)
if is_append:
self._clslevel[cls].append(event_key._listen_fn)
else:
self._clslevel[cls].appendleft(event_key._listen_fn)
registry._stored_in_collection(event_key, self)
def insert(self, event_key: _EventKey[_ET], propagate: bool) -> None:
self._do_insert_or_append(event_key, is_append=False)
def append(self, event_key: _EventKey[_ET], propagate: bool) -> None:
self._do_insert_or_append(event_key, is_append=True)
def update_subclass(self, target: Type[_ET]) -> None:
if target not in self._clslevel:
if getattr(target, "_sa_propagate_class_events", True):
self._clslevel[target] = collections.deque()
else:
self._clslevel[target] = _empty_collection()
clslevel = self._clslevel[target]
cls: Type[_ET]
for cls in target.__mro__[1:]:
if cls in self._clslevel:
clslevel.extend(
[fn for fn in self._clslevel[cls] if fn not in clslevel]
)
def remove(self, event_key: _EventKey[_ET]) -> None:
target = event_key.dispatch_target
cls: Type[_ET]
for cls in util.walk_subclasses(target):
if cls in self._clslevel:
self._clslevel[cls].remove(event_key._listen_fn)
registry._removed_from_collection(event_key, self)
def clear(self) -> None:
"""Clear all class level listeners"""
to_clear: Set[_ListenerFnType] = set()
for dispatcher in self._clslevel.values():
to_clear.update(dispatcher)
dispatcher.clear()
registry._clear(self, to_clear)
def for_modify(self, obj: _Dispatch[_ET]) -> _ClsLevelDispatch[_ET]:
"""Return an event collection which can be modified.
For _ClsLevelDispatch at the class level of
a dispatcher, this returns self.
"""
return self
class _InstanceLevelDispatch(RefCollection[_ET], Collection[_ListenerFnType]):
__slots__ = ()
parent: _ClsLevelDispatch[_ET]
def _adjust_fn_spec(
self, fn: _ListenerFnType, named: bool
) -> _ListenerFnType:
return self.parent._adjust_fn_spec(fn, named)
def __contains__(self, item: Any) -> bool:
raise NotImplementedError()
def __len__(self) -> int:
raise NotImplementedError()
def __iter__(self) -> Iterator[_ListenerFnType]:
raise NotImplementedError()
def __bool__(self) -> bool:
raise NotImplementedError()
def exec_once(self, *args: Any, **kw: Any) -> None:
raise NotImplementedError()
def exec_once_unless_exception(self, *args: Any, **kw: Any) -> None:
raise NotImplementedError()
def _exec_w_sync_on_first_run(self, *args: Any, **kw: Any) -> None:
raise NotImplementedError()
def __call__(self, *args: Any, **kw: Any) -> None:
raise NotImplementedError()
def insert(self, event_key: _EventKey[_ET], propagate: bool) -> None:
raise NotImplementedError()
def append(self, event_key: _EventKey[_ET], propagate: bool) -> None:
raise NotImplementedError()
def remove(self, event_key: _EventKey[_ET]) -> None:
raise NotImplementedError()
def for_modify(
self, obj: _DispatchCommon[_ET]
) -> _InstanceLevelDispatch[_ET]:
"""Return an event collection which can be modified.
For _ClsLevelDispatch at the class level of
a dispatcher, this returns self.
"""
return self
class _EmptyListener(_InstanceLevelDispatch[_ET]):
"""Serves as a proxy interface to the events
served by a _ClsLevelDispatch, when there are no
instance-level events present.
Is replaced by _ListenerCollection when instance-level
events are added.
"""
__slots__ = "parent", "parent_listeners", "name"
propagate: FrozenSet[_ListenerFnType] = frozenset()
listeners: Tuple[()] = ()
parent: _ClsLevelDispatch[_ET]
parent_listeners: _ListenerFnSequenceType[_ListenerFnType]
name: str
def __init__(self, parent: _ClsLevelDispatch[_ET], target_cls: Type[_ET]):
if target_cls not in parent._clslevel:
parent.update_subclass(target_cls)
self.parent = parent
self.parent_listeners = parent._clslevel[target_cls]
self.name = parent.name
def for_modify(
self, obj: _DispatchCommon[_ET]
) -> _ListenerCollection[_ET]:
"""Return an event collection which can be modified.
For _EmptyListener at the instance level of
a dispatcher, this generates a new
_ListenerCollection, applies it to the instance,
and returns it.
"""
obj = cast("_Dispatch[_ET]", obj)
assert obj._instance_cls is not None
result = _ListenerCollection(self.parent, obj._instance_cls)
if getattr(obj, self.name) is self:
setattr(obj, self.name, result)
else:
assert isinstance(getattr(obj, self.name), _JoinedListener)
return result
def _needs_modify(self, *args: Any, **kw: Any) -> NoReturn:
raise NotImplementedError("need to call for_modify()")
def exec_once(self, *args: Any, **kw: Any) -> NoReturn:
self._needs_modify(*args, **kw)
def exec_once_unless_exception(self, *args: Any, **kw: Any) -> NoReturn:
self._needs_modify(*args, **kw)
def insert(self, *args: Any, **kw: Any) -> NoReturn:
self._needs_modify(*args, **kw)
def append(self, *args: Any, **kw: Any) -> NoReturn:
self._needs_modify(*args, **kw)
def remove(self, *args: Any, **kw: Any) -> NoReturn:
self._needs_modify(*args, **kw)
def clear(self, *args: Any, **kw: Any) -> NoReturn:
self._needs_modify(*args, **kw)
def __call__(self, *args: Any, **kw: Any) -> None:
"""Execute this event."""
for fn in self.parent_listeners:
fn(*args, **kw)
def __contains__(self, item: Any) -> bool:
return item in self.parent_listeners
def __len__(self) -> int:
return len(self.parent_listeners)
def __iter__(self) -> Iterator[_ListenerFnType]:
return iter(self.parent_listeners)
def __bool__(self) -> bool:
return bool(self.parent_listeners)
class _MutexProtocol(Protocol):
def __enter__(self) -> bool: ...
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> Optional[bool]: ...
class _CompoundListener(_InstanceLevelDispatch[_ET]):
__slots__ = (
"_exec_once_mutex",
"_exec_once",
"_exec_w_sync_once",
"_is_asyncio",
)
_exec_once_mutex: _MutexProtocol
parent_listeners: Collection[_ListenerFnType]
listeners: Collection[_ListenerFnType]
_exec_once: bool
_exec_w_sync_once: bool
def __init__(self, *arg: Any, **kw: Any):
super().__init__(*arg, **kw)
self._is_asyncio = False
def _set_asyncio(self) -> None:
self._is_asyncio = True
def _memoized_attr__exec_once_mutex(self) -> _MutexProtocol:
if self._is_asyncio:
return AsyncAdaptedLock()
else:
return threading.Lock()
def _exec_once_impl(
self, retry_on_exception: bool, *args: Any, **kw: Any
) -> None:
with self._exec_once_mutex:
if not self._exec_once:
try:
self(*args, **kw)
exception = False
except:
exception = True
raise
finally:
if not exception or not retry_on_exception:
self._exec_once = True
def exec_once(self, *args: Any, **kw: Any) -> None:
"""Execute this event, but only if it has not been
executed already for this collection."""
if not self._exec_once:
self._exec_once_impl(False, *args, **kw)
def exec_once_unless_exception(self, *args: Any, **kw: Any) -> None:
"""Execute this event, but only if it has not been
executed already for this collection, or was called
by a previous exec_once_unless_exception call and
raised an exception.
If exec_once was already called, then this method will never run
the callable regardless of whether it raised or not.
.. versionadded:: 1.3.8
"""
if not self._exec_once:
self._exec_once_impl(True, *args, **kw)
def _exec_w_sync_on_first_run(self, *args: Any, **kw: Any) -> None:
"""Execute this event, and use a mutex if it has not been
executed already for this collection, or was called
by a previous _exec_w_sync_on_first_run call and
raised an exception.
If _exec_w_sync_on_first_run was already called and didn't raise an
exception, then a mutex is not used.
.. versionadded:: 1.4.11
"""
if not self._exec_w_sync_once:
with self._exec_once_mutex:
try:
self(*args, **kw)
except:
raise
else:
self._exec_w_sync_once = True
else:
self(*args, **kw)
def __call__(self, *args: Any, **kw: Any) -> None:
"""Execute this event."""
for fn in self.parent_listeners:
fn(*args, **kw)
for fn in self.listeners:
fn(*args, **kw)
def __contains__(self, item: Any) -> bool:
return item in self.parent_listeners or item in self.listeners
def __len__(self) -> int:
return len(self.parent_listeners) + len(self.listeners)
def __iter__(self) -> Iterator[_ListenerFnType]:
return chain(self.parent_listeners, self.listeners)
def __bool__(self) -> bool:
return bool(self.listeners or self.parent_listeners)
class _ListenerCollection(_CompoundListener[_ET]):
"""Instance-level attributes on instances of :class:`._Dispatch`.
Represents a collection of listeners.
As of 0.7.9, _ListenerCollection is only first
created via the _EmptyListener.for_modify() method.
"""
__slots__ = (
"parent_listeners",
"parent",
"name",
"listeners",
"propagate",
"__weakref__",
)
parent_listeners: Collection[_ListenerFnType]
parent: _ClsLevelDispatch[_ET]
name: str
listeners: Deque[_ListenerFnType]
propagate: Set[_ListenerFnType]
def __init__(self, parent: _ClsLevelDispatch[_ET], target_cls: Type[_ET]):
super().__init__()
if target_cls not in parent._clslevel:
parent.update_subclass(target_cls)
self._exec_once = False
self._exec_w_sync_once = False
self.parent_listeners = parent._clslevel[target_cls]
self.parent = parent
self.name = parent.name
self.listeners = collections.deque()
self.propagate = set()
def for_modify(
self, obj: _DispatchCommon[_ET]
) -> _ListenerCollection[_ET]:
"""Return an event collection which can be modified.
For _ListenerCollection at the instance level of
a dispatcher, this returns self.
"""
return self
def _update(
self, other: _ListenerCollection[_ET], only_propagate: bool = True
) -> None:
"""Populate from the listeners in another :class:`_Dispatch`
object."""
existing_listeners = self.listeners
existing_listener_set = set(existing_listeners)
self.propagate.update(other.propagate)
other_listeners = [
l
for l in other.listeners
if l not in existing_listener_set
and not only_propagate
or l in self.propagate
]
existing_listeners.extend(other_listeners)
if other._is_asyncio:
self._set_asyncio()
to_associate = other.propagate.union(other_listeners)
registry._stored_in_collection_multi(self, other, to_associate)
def insert(self, event_key: _EventKey[_ET], propagate: bool) -> None:
if event_key.prepend_to_list(self, self.listeners):
if propagate:
self.propagate.add(event_key._listen_fn)
def append(self, event_key: _EventKey[_ET], propagate: bool) -> None:
if event_key.append_to_list(self, self.listeners):
if propagate:
self.propagate.add(event_key._listen_fn)
def remove(self, event_key: _EventKey[_ET]) -> None:
self.listeners.remove(event_key._listen_fn)
self.propagate.discard(event_key._listen_fn)
registry._removed_from_collection(event_key, self)
def clear(self) -> None:
registry._clear(self, self.listeners)
self.propagate.clear()
self.listeners.clear()
class _JoinedListener(_CompoundListener[_ET]):
__slots__ = "parent_dispatch", "name", "local", "parent_listeners"
parent_dispatch: _DispatchCommon[_ET]
name: str
local: _InstanceLevelDispatch[_ET]
parent_listeners: Collection[_ListenerFnType]
def __init__(
self,
parent_dispatch: _DispatchCommon[_ET],
name: str,
local: _EmptyListener[_ET],
):
self._exec_once = False
self.parent_dispatch = parent_dispatch
self.name = name
self.local = local
self.parent_listeners = self.local
if not typing.TYPE_CHECKING:
# first error, I don't really understand:
# Signature of "listeners" incompatible with
# supertype "_CompoundListener" [override]
# the name / return type are exactly the same
# second error is getattr_isn't typed, the cast() here
# adds too much method overhead
@property
def listeners(self) -> Collection[_ListenerFnType]:
return getattr(self.parent_dispatch, self.name)
def _adjust_fn_spec(
self, fn: _ListenerFnType, named: bool
) -> _ListenerFnType:
return self.local._adjust_fn_spec(fn, named)
def for_modify(self, obj: _DispatchCommon[_ET]) -> _JoinedListener[_ET]:
self.local = self.parent_listeners = self.local.for_modify(obj)
return self
def insert(self, event_key: _EventKey[_ET], propagate: bool) -> None:
self.local.insert(event_key, propagate)
def append(self, event_key: _EventKey[_ET], propagate: bool) -> None:
self.local.append(event_key, propagate)
def remove(self, event_key: _EventKey[_ET]) -> None:
self.local.remove(event_key)
def clear(self) -> None:
raise NotImplementedError()

View File

@ -0,0 +1,472 @@
# event/base.py
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
# the MIT License: https://www.opensource.org/licenses/mit-license.php
"""Base implementation classes.
The public-facing ``Events`` serves as the base class for an event interface;
its public attributes represent different kinds of events. These attributes
are mirrored onto a ``_Dispatch`` class, which serves as a container for
collections of listener functions. These collections are represented both
at the class level of a particular ``_Dispatch`` class as well as within
instances of ``_Dispatch``.
"""
from __future__ import annotations
import typing
from typing import Any
from typing import cast
from typing import Dict
from typing import Generic
from typing import Iterator
from typing import List
from typing import Mapping
from typing import MutableMapping
from typing import Optional
from typing import overload
from typing import Tuple
from typing import Type
from typing import Union
import weakref
from .attr import _ClsLevelDispatch
from .attr import _EmptyListener
from .attr import _InstanceLevelDispatch
from .attr import _JoinedListener
from .registry import _ET
from .registry import _EventKey
from .. import util
from ..util.typing import Literal
_registrars: MutableMapping[str, List[Type[_HasEventsDispatch[Any]]]] = (
util.defaultdict(list)
)
def _is_event_name(name: str) -> bool:
# _sa_event prefix is special to support internal-only event names.
# most event names are just plain method names that aren't
# underscored.
return (
not name.startswith("_") and name != "dispatch"
) or name.startswith("_sa_event")
class _UnpickleDispatch:
"""Serializable callable that re-generates an instance of
:class:`_Dispatch` given a particular :class:`.Events` subclass.
"""
def __call__(self, _instance_cls: Type[_ET]) -> _Dispatch[_ET]:
for cls in _instance_cls.__mro__:
if "dispatch" in cls.__dict__:
return cast(
"_Dispatch[_ET]", cls.__dict__["dispatch"].dispatch
)._for_class(_instance_cls)
else:
raise AttributeError("No class with a 'dispatch' member present.")
class _DispatchCommon(Generic[_ET]):
__slots__ = ()
_instance_cls: Optional[Type[_ET]]
def _join(self, other: _DispatchCommon[_ET]) -> _JoinedDispatcher[_ET]:
raise NotImplementedError()
def __getattr__(self, name: str) -> _InstanceLevelDispatch[_ET]:
raise NotImplementedError()
@property
def _events(self) -> Type[_HasEventsDispatch[_ET]]:
raise NotImplementedError()
class _Dispatch(_DispatchCommon[_ET]):
"""Mirror the event listening definitions of an Events class with
listener collections.
Classes which define a "dispatch" member will return a
non-instantiated :class:`._Dispatch` subclass when the member
is accessed at the class level. When the "dispatch" member is
accessed at the instance level of its owner, an instance
of the :class:`._Dispatch` class is returned.
A :class:`._Dispatch` class is generated for each :class:`.Events`
class defined, by the :meth:`._HasEventsDispatch._create_dispatcher_class`
method. The original :class:`.Events` classes remain untouched.
This decouples the construction of :class:`.Events` subclasses from
the implementation used by the event internals, and allows
inspecting tools like Sphinx to work in an unsurprising
way against the public API.
"""
# "active_history" is an ORM case we add here. ideally a better
# system would be in place for ad-hoc attributes.
__slots__ = "_parent", "_instance_cls", "__dict__", "_empty_listeners"
_active_history: bool
_empty_listener_reg: MutableMapping[
Type[_ET], Dict[str, _EmptyListener[_ET]]
] = weakref.WeakKeyDictionary()
_empty_listeners: Dict[str, _EmptyListener[_ET]]
_event_names: List[str]
_instance_cls: Optional[Type[_ET]]
_joined_dispatch_cls: Type[_JoinedDispatcher[_ET]]
_events: Type[_HasEventsDispatch[_ET]]
"""reference back to the Events class.
Bidirectional against _HasEventsDispatch.dispatch
"""
def __init__(
self,
parent: Optional[_Dispatch[_ET]],
instance_cls: Optional[Type[_ET]] = None,
):
self._parent = parent
self._instance_cls = instance_cls
if instance_cls:
assert parent is not None
try:
self._empty_listeners = self._empty_listener_reg[instance_cls]
except KeyError:
self._empty_listeners = self._empty_listener_reg[
instance_cls
] = {
ls.name: _EmptyListener(ls, instance_cls)
for ls in parent._event_descriptors
}
else:
self._empty_listeners = {}
def __getattr__(self, name: str) -> _InstanceLevelDispatch[_ET]:
# Assign EmptyListeners as attributes on demand
# to reduce startup time for new dispatch objects.
try:
ls = self._empty_listeners[name]
except KeyError:
raise AttributeError(name)
else:
setattr(self, ls.name, ls)
return ls
@property
def _event_descriptors(self) -> Iterator[_ClsLevelDispatch[_ET]]:
for k in self._event_names:
# Yield _ClsLevelDispatch related
# to relevant event name.
yield getattr(self, k)
def _listen(self, event_key: _EventKey[_ET], **kw: Any) -> None:
return self._events._listen(event_key, **kw)
def _for_class(self, instance_cls: Type[_ET]) -> _Dispatch[_ET]:
return self.__class__(self, instance_cls)
def _for_instance(self, instance: _ET) -> _Dispatch[_ET]:
instance_cls = instance.__class__
return self._for_class(instance_cls)
def _join(self, other: _DispatchCommon[_ET]) -> _JoinedDispatcher[_ET]:
"""Create a 'join' of this :class:`._Dispatch` and another.
This new dispatcher will dispatch events to both
:class:`._Dispatch` objects.
"""
assert "_joined_dispatch_cls" in self.__class__.__dict__
return self._joined_dispatch_cls(self, other)
def __reduce__(self) -> Union[str, Tuple[Any, ...]]:
return _UnpickleDispatch(), (self._instance_cls,)
def _update(
self, other: _Dispatch[_ET], only_propagate: bool = True
) -> None:
"""Populate from the listeners in another :class:`_Dispatch`
object."""
for ls in other._event_descriptors:
if isinstance(ls, _EmptyListener):
continue
getattr(self, ls.name).for_modify(self)._update(
ls, only_propagate=only_propagate
)
def _clear(self) -> None:
for ls in self._event_descriptors:
ls.for_modify(self).clear()
def _remove_dispatcher(cls: Type[_HasEventsDispatch[_ET]]) -> None:
for k in cls.dispatch._event_names:
_registrars[k].remove(cls)
if not _registrars[k]:
del _registrars[k]
class _HasEventsDispatch(Generic[_ET]):
_dispatch_target: Optional[Type[_ET]]
"""class which will receive the .dispatch collection"""
dispatch: _Dispatch[_ET]
"""reference back to the _Dispatch class.
Bidirectional against _Dispatch._events
"""
if typing.TYPE_CHECKING:
def __getattr__(self, name: str) -> _InstanceLevelDispatch[_ET]: ...
def __init_subclass__(cls) -> None:
"""Intercept new Event subclasses and create associated _Dispatch
classes."""
cls._create_dispatcher_class(cls.__name__, cls.__bases__, cls.__dict__)
@classmethod
def _accept_with(
cls, target: Union[_ET, Type[_ET]], identifier: str
) -> Optional[Union[_ET, Type[_ET]]]:
raise NotImplementedError()
@classmethod
def _listen(
cls,
event_key: _EventKey[_ET],
*,
propagate: bool = False,
insert: bool = False,
named: bool = False,
asyncio: bool = False,
) -> None:
raise NotImplementedError()
@staticmethod
def _set_dispatch(
klass: Type[_HasEventsDispatch[_ET]],
dispatch_cls: Type[_Dispatch[_ET]],
) -> _Dispatch[_ET]:
# This allows an Events subclass to define additional utility
# methods made available to the target via
# "self.dispatch._events.<utilitymethod>"
# @staticmethod to allow easy "super" calls while in a metaclass
# constructor.
klass.dispatch = dispatch_cls(None)
dispatch_cls._events = klass
return klass.dispatch
@classmethod
def _create_dispatcher_class(
cls, classname: str, bases: Tuple[type, ...], dict_: Mapping[str, Any]
) -> None:
"""Create a :class:`._Dispatch` class corresponding to an
:class:`.Events` class."""
# there's all kinds of ways to do this,
# i.e. make a Dispatch class that shares the '_listen' method
# of the Event class, this is the straight monkeypatch.
if hasattr(cls, "dispatch"):
dispatch_base = cls.dispatch.__class__
else:
dispatch_base = _Dispatch
event_names = [k for k in dict_ if _is_event_name(k)]
dispatch_cls = cast(
"Type[_Dispatch[_ET]]",
type(
"%sDispatch" % classname,
(dispatch_base,),
{"__slots__": event_names},
),
)
dispatch_cls._event_names = event_names
dispatch_inst = cls._set_dispatch(cls, dispatch_cls)
for k in dispatch_cls._event_names:
setattr(dispatch_inst, k, _ClsLevelDispatch(cls, dict_[k]))
_registrars[k].append(cls)
for super_ in dispatch_cls.__bases__:
if issubclass(super_, _Dispatch) and super_ is not _Dispatch:
for ls in super_._events.dispatch._event_descriptors:
setattr(dispatch_inst, ls.name, ls)
dispatch_cls._event_names.append(ls.name)
if getattr(cls, "_dispatch_target", None):
dispatch_target_cls = cls._dispatch_target
assert dispatch_target_cls is not None
if (
hasattr(dispatch_target_cls, "__slots__")
and "_slots_dispatch" in dispatch_target_cls.__slots__
):
dispatch_target_cls.dispatch = slots_dispatcher(cls)
else:
dispatch_target_cls.dispatch = dispatcher(cls)
klass = type(
"Joined%s" % dispatch_cls.__name__,
(_JoinedDispatcher,),
{"__slots__": event_names},
)
dispatch_cls._joined_dispatch_cls = klass
# establish pickle capability by adding it to this module
globals()[klass.__name__] = klass
class _JoinedDispatcher(_DispatchCommon[_ET]):
"""Represent a connection between two _Dispatch objects."""
__slots__ = "local", "parent", "_instance_cls"
local: _DispatchCommon[_ET]
parent: _DispatchCommon[_ET]
_instance_cls: Optional[Type[_ET]]
def __init__(
self, local: _DispatchCommon[_ET], parent: _DispatchCommon[_ET]
):
self.local = local
self.parent = parent
self._instance_cls = self.local._instance_cls
def __reduce__(self) -> Any:
return (self.__class__, (self.local, self.parent))
def __getattr__(self, name: str) -> _JoinedListener[_ET]:
# Assign _JoinedListeners as attributes on demand
# to reduce startup time for new dispatch objects.
ls = getattr(self.local, name)
jl = _JoinedListener(self.parent, ls.name, ls)
setattr(self, ls.name, jl)
return jl
def _listen(self, event_key: _EventKey[_ET], **kw: Any) -> None:
return self.parent._listen(event_key, **kw)
@property
def _events(self) -> Type[_HasEventsDispatch[_ET]]:
return self.parent._events
class Events(_HasEventsDispatch[_ET]):
"""Define event listening functions for a particular target type."""
@classmethod
def _accept_with(
cls, target: Union[_ET, Type[_ET]], identifier: str
) -> Optional[Union[_ET, Type[_ET]]]:
def dispatch_is(*types: Type[Any]) -> bool:
return all(isinstance(target.dispatch, t) for t in types)
def dispatch_parent_is(t: Type[Any]) -> bool:
parent = cast("_JoinedDispatcher[_ET]", target.dispatch).parent
while isinstance(parent, _JoinedDispatcher):
parent = cast("_JoinedDispatcher[_ET]", parent).parent
return isinstance(parent, t)
# Mapper, ClassManager, Session override this to
# also accept classes, scoped_sessions, sessionmakers, etc.
if hasattr(target, "dispatch"):
if (
dispatch_is(cls.dispatch.__class__)
or dispatch_is(type, cls.dispatch.__class__)
or (
dispatch_is(_JoinedDispatcher)
and dispatch_parent_is(cls.dispatch.__class__)
)
):
return target
return None
@classmethod
def _listen(
cls,
event_key: _EventKey[_ET],
*,
propagate: bool = False,
insert: bool = False,
named: bool = False,
asyncio: bool = False,
) -> None:
event_key.base_listen(
propagate=propagate, insert=insert, named=named, asyncio=asyncio
)
@classmethod
def _remove(cls, event_key: _EventKey[_ET]) -> None:
event_key.remove()
@classmethod
def _clear(cls) -> None:
cls.dispatch._clear()
class dispatcher(Generic[_ET]):
"""Descriptor used by target classes to
deliver the _Dispatch class at the class level
and produce new _Dispatch instances for target
instances.
"""
def __init__(self, events: Type[_HasEventsDispatch[_ET]]):
self.dispatch = events.dispatch
self.events = events
@overload
def __get__(
self, obj: Literal[None], cls: Type[Any]
) -> Type[_Dispatch[_ET]]: ...
@overload
def __get__(self, obj: Any, cls: Type[Any]) -> _DispatchCommon[_ET]: ...
def __get__(self, obj: Any, cls: Type[Any]) -> Any:
if obj is None:
return self.dispatch
disp = self.dispatch._for_instance(obj)
try:
obj.__dict__["dispatch"] = disp
except AttributeError as ae:
raise TypeError(
"target %r doesn't have __dict__, should it be "
"defining _slots_dispatch?" % (obj,)
) from ae
return disp
class slots_dispatcher(dispatcher[_ET]):
def __get__(self, obj: Any, cls: Type[Any]) -> Any:
if obj is None:
return self.dispatch
if hasattr(obj, "_slots_dispatch"):
return obj._slots_dispatch
disp = self.dispatch._for_instance(obj)
obj._slots_dispatch = disp
return disp

View File

@ -0,0 +1,246 @@
# event/legacy.py
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
# the MIT License: https://www.opensource.org/licenses/mit-license.php
"""Routines to handle adaption of legacy call signatures,
generation of deprecation notes and docstrings.
"""
from __future__ import annotations
import typing
from typing import Any
from typing import Callable
from typing import List
from typing import Optional
from typing import Tuple
from typing import Type
from .registry import _ET
from .registry import _ListenerFnType
from .. import util
from ..util.compat import FullArgSpec
if typing.TYPE_CHECKING:
from .attr import _ClsLevelDispatch
from .base import _HasEventsDispatch
_LegacySignatureType = Tuple[str, List[str], Optional[Callable[..., Any]]]
def _legacy_signature(
since: str,
argnames: List[str],
converter: Optional[Callable[..., Any]] = None,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
"""legacy sig decorator
:param since: string version for deprecation warning
:param argnames: list of strings, which is *all* arguments that the legacy
version accepted, including arguments that are still there
:param converter: lambda that will accept tuple of this full arg signature
and return tuple of new arg signature.
"""
def leg(fn: Callable[..., Any]) -> Callable[..., Any]:
if not hasattr(fn, "_legacy_signatures"):
fn._legacy_signatures = [] # type: ignore[attr-defined]
fn._legacy_signatures.append((since, argnames, converter)) # type: ignore[attr-defined] # noqa: E501
return fn
return leg
def _wrap_fn_for_legacy(
dispatch_collection: _ClsLevelDispatch[_ET],
fn: _ListenerFnType,
argspec: FullArgSpec,
) -> _ListenerFnType:
for since, argnames, conv in dispatch_collection.legacy_signatures:
if argnames[-1] == "**kw":
has_kw = True
argnames = argnames[0:-1]
else:
has_kw = False
if len(argnames) == len(argspec.args) and has_kw is bool(
argspec.varkw
):
formatted_def = "def %s(%s%s)" % (
dispatch_collection.name,
", ".join(dispatch_collection.arg_names),
", **kw" if has_kw else "",
)
warning_txt = (
'The argument signature for the "%s.%s" event listener '
"has changed as of version %s, and conversion for "
"the old argument signature will be removed in a "
'future release. The new signature is "%s"'
% (
dispatch_collection.clsname,
dispatch_collection.name,
since,
formatted_def,
)
)
if conv is not None:
assert not has_kw
def wrap_leg(*args: Any, **kw: Any) -> Any:
util.warn_deprecated(warning_txt, version=since)
assert conv is not None
return fn(*conv(*args))
else:
def wrap_leg(*args: Any, **kw: Any) -> Any:
util.warn_deprecated(warning_txt, version=since)
argdict = dict(zip(dispatch_collection.arg_names, args))
args_from_dict = [argdict[name] for name in argnames]
if has_kw:
return fn(*args_from_dict, **kw)
else:
return fn(*args_from_dict)
return wrap_leg
else:
return fn
def _indent(text: str, indent: str) -> str:
return "\n".join(indent + line for line in text.split("\n"))
def _standard_listen_example(
dispatch_collection: _ClsLevelDispatch[_ET],
sample_target: Any,
fn: _ListenerFnType,
) -> str:
example_kw_arg = _indent(
"\n".join(
"%(arg)s = kw['%(arg)s']" % {"arg": arg}
for arg in dispatch_collection.arg_names[0:2]
),
" ",
)
if dispatch_collection.legacy_signatures:
current_since = max(
since
for since, args, conv in dispatch_collection.legacy_signatures
)
else:
current_since = None
text = (
"from sqlalchemy import event\n\n\n"
"@event.listens_for(%(sample_target)s, '%(event_name)s')\n"
"def receive_%(event_name)s("
"%(named_event_arguments)s%(has_kw_arguments)s):\n"
" \"listen for the '%(event_name)s' event\"\n"
"\n # ... (event handling logic) ...\n"
)
text %= {
"current_since": (
" (arguments as of %s)" % current_since if current_since else ""
),
"event_name": fn.__name__,
"has_kw_arguments": ", **kw" if dispatch_collection.has_kw else "",
"named_event_arguments": ", ".join(dispatch_collection.arg_names),
"example_kw_arg": example_kw_arg,
"sample_target": sample_target,
}
return text
def _legacy_listen_examples(
dispatch_collection: _ClsLevelDispatch[_ET],
sample_target: str,
fn: _ListenerFnType,
) -> str:
text = ""
for since, args, conv in dispatch_collection.legacy_signatures:
text += (
"\n# DEPRECATED calling style (pre-%(since)s, "
"will be removed in a future release)\n"
"@event.listens_for(%(sample_target)s, '%(event_name)s')\n"
"def receive_%(event_name)s("
"%(named_event_arguments)s%(has_kw_arguments)s):\n"
" \"listen for the '%(event_name)s' event\"\n"
"\n # ... (event handling logic) ...\n"
% {
"since": since,
"event_name": fn.__name__,
"has_kw_arguments": (
" **kw" if dispatch_collection.has_kw else ""
),
"named_event_arguments": ", ".join(args),
"sample_target": sample_target,
}
)
return text
def _version_signature_changes(
parent_dispatch_cls: Type[_HasEventsDispatch[_ET]],
dispatch_collection: _ClsLevelDispatch[_ET],
) -> str:
since, args, conv = dispatch_collection.legacy_signatures[0]
return (
"\n.. versionchanged:: %(since)s\n"
" The :meth:`.%(clsname)s.%(event_name)s` event now accepts the \n"
" arguments %(named_event_arguments)s%(has_kw_arguments)s.\n"
" Support for listener functions which accept the previous \n"
' argument signature(s) listed above as "deprecated" will be \n'
" removed in a future release."
% {
"since": since,
"clsname": parent_dispatch_cls.__name__,
"event_name": dispatch_collection.name,
"named_event_arguments": ", ".join(
":paramref:`.%(clsname)s.%(event_name)s.%(param_name)s`"
% {
"clsname": parent_dispatch_cls.__name__,
"event_name": dispatch_collection.name,
"param_name": param_name,
}
for param_name in dispatch_collection.arg_names
),
"has_kw_arguments": ", **kw" if dispatch_collection.has_kw else "",
}
)
def _augment_fn_docs(
dispatch_collection: _ClsLevelDispatch[_ET],
parent_dispatch_cls: Type[_HasEventsDispatch[_ET]],
fn: _ListenerFnType,
) -> str:
header = (
".. container:: event_signatures\n\n"
" Example argument forms::\n"
"\n"
)
sample_target = getattr(parent_dispatch_cls, "_target_class_doc", "obj")
text = header + _indent(
_standard_listen_example(dispatch_collection, sample_target, fn),
" " * 8,
)
if dispatch_collection.legacy_signatures:
text += _indent(
_legacy_listen_examples(dispatch_collection, sample_target, fn),
" " * 8,
)
text += _version_signature_changes(
parent_dispatch_cls, dispatch_collection
)
return util.inject_docstring_text(fn.__doc__, text, 1)

View File

@ -0,0 +1,390 @@
# event/registry.py
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
# <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
# the MIT License: https://www.opensource.org/licenses/mit-license.php
"""Provides managed registration services on behalf of :func:`.listen`
arguments.
By "managed registration", we mean that event listening functions and
other objects can be added to various collections in such a way that their
membership in all those collections can be revoked at once, based on
an equivalent :class:`._EventKey`.
"""
from __future__ import annotations
import collections
import types
import typing
from typing import Any
from typing import Callable
from typing import cast
from typing import Deque
from typing import Dict
from typing import Generic
from typing import Iterable
from typing import Optional
from typing import Tuple
from typing import TypeVar
from typing import Union
import weakref
from .. import exc
from .. import util
if typing.TYPE_CHECKING:
from .attr import RefCollection
from .base import dispatcher
_ListenerFnType = Callable[..., Any]
_ListenerFnKeyType = Union[int, Tuple[int, int]]
_EventKeyTupleType = Tuple[int, str, _ListenerFnKeyType]
_ET = TypeVar("_ET", bound="EventTarget")
class EventTarget:
"""represents an event target, that is, something we can listen on
either with that target as a class or as an instance.
Examples include: Connection, Mapper, Table, Session,
InstrumentedAttribute, Engine, Pool, Dialect.
"""
__slots__ = ()
dispatch: dispatcher[Any]
_RefCollectionToListenerType = Dict[
"weakref.ref[RefCollection[Any]]",
"weakref.ref[_ListenerFnType]",
]
_key_to_collection: Dict[_EventKeyTupleType, _RefCollectionToListenerType] = (
collections.defaultdict(dict)
)
"""
Given an original listen() argument, can locate all
listener collections and the listener fn contained
(target, identifier, fn) -> {
ref(listenercollection) -> ref(listener_fn)
ref(listenercollection) -> ref(listener_fn)
ref(listenercollection) -> ref(listener_fn)
}
"""
_ListenerToEventKeyType = Dict[
"weakref.ref[_ListenerFnType]",
_EventKeyTupleType,
]
_collection_to_key: Dict[
weakref.ref[RefCollection[Any]],
_ListenerToEventKeyType,
] = collections.defaultdict(dict)
"""
Given a _ListenerCollection or _ClsLevelListener, can locate
all the original listen() arguments and the listener fn contained
ref(listenercollection) -> {
ref(listener_fn) -> (target, identifier, fn),
ref(listener_fn) -> (target, identifier, fn),
ref(listener_fn) -> (target, identifier, fn),
}
"""
def _collection_gced(ref: weakref.ref[Any]) -> None:
# defaultdict, so can't get a KeyError
if not _collection_to_key or ref not in _collection_to_key:
return
ref = cast("weakref.ref[RefCollection[EventTarget]]", ref)
listener_to_key = _collection_to_key.pop(ref)
for key in listener_to_key.values():
if key in _key_to_collection:
# defaultdict, so can't get a KeyError
dispatch_reg = _key_to_collection[key]
dispatch_reg.pop(ref)
if not dispatch_reg:
_key_to_collection.pop(key)
def _stored_in_collection(
event_key: _EventKey[_ET], owner: RefCollection[_ET]
) -> bool:
key = event_key._key
dispatch_reg = _key_to_collection[key]
owner_ref = owner.ref
listen_ref = weakref.ref(event_key._listen_fn)
if owner_ref in dispatch_reg:
return False
dispatch_reg[owner_ref] = listen_ref
listener_to_key = _collection_to_key[owner_ref]
listener_to_key[listen_ref] = key
return True
def _removed_from_collection(
event_key: _EventKey[_ET], owner: RefCollection[_ET]
) -> None:
key = event_key._key
dispatch_reg = _key_to_collection[key]
listen_ref = weakref.ref(event_key._listen_fn)
owner_ref = owner.ref
dispatch_reg.pop(owner_ref, None)
if not dispatch_reg:
del _key_to_collection[key]
if owner_ref in _collection_to_key:
listener_to_key = _collection_to_key[owner_ref]
# see #12216 - this guards against a removal that already occurred
# here. however, I cannot come up with a test that shows any negative
# side effects occurring from this removal happening, even though an
# event key may still be referenced from a clsleveldispatch here
listener_to_key.pop(listen_ref, None)
def _stored_in_collection_multi(
newowner: RefCollection[_ET],
oldowner: RefCollection[_ET],
elements: Iterable[_ListenerFnType],
) -> None:
if not elements:
return
oldowner_ref = oldowner.ref
newowner_ref = newowner.ref
old_listener_to_key = _collection_to_key[oldowner_ref]
new_listener_to_key = _collection_to_key[newowner_ref]
for listen_fn in elements:
listen_ref = weakref.ref(listen_fn)
try:
key = old_listener_to_key[listen_ref]
except KeyError:
# can occur during interpreter shutdown.
# see #6740
continue
try:
dispatch_reg = _key_to_collection[key]
except KeyError:
continue
if newowner_ref in dispatch_reg:
assert dispatch_reg[newowner_ref] == listen_ref
else:
dispatch_reg[newowner_ref] = listen_ref
new_listener_to_key[listen_ref] = key
def _clear(
owner: RefCollection[_ET],
elements: Iterable[_ListenerFnType],
) -> None:
if not elements:
return
owner_ref = owner.ref
listener_to_key = _collection_to_key[owner_ref]
for listen_fn in elements:
listen_ref = weakref.ref(listen_fn)
key = listener_to_key[listen_ref]
dispatch_reg = _key_to_collection[key]
dispatch_reg.pop(owner_ref, None)
if not dispatch_reg:
del _key_to_collection[key]
class _EventKey(Generic[_ET]):
"""Represent :func:`.listen` arguments."""
__slots__ = (
"target",
"identifier",
"fn",
"fn_key",
"fn_wrap",
"dispatch_target",
)
target: _ET
identifier: str
fn: _ListenerFnType
fn_key: _ListenerFnKeyType
dispatch_target: Any
_fn_wrap: Optional[_ListenerFnType]
def __init__(
self,
target: _ET,
identifier: str,
fn: _ListenerFnType,
dispatch_target: Any,
_fn_wrap: Optional[_ListenerFnType] = None,
):
self.target = target
self.identifier = identifier
self.fn = fn
if isinstance(fn, types.MethodType):
self.fn_key = id(fn.__func__), id(fn.__self__)
else:
self.fn_key = id(fn)
self.fn_wrap = _fn_wrap
self.dispatch_target = dispatch_target
@property
def _key(self) -> _EventKeyTupleType:
return (id(self.target), self.identifier, self.fn_key)
def with_wrapper(self, fn_wrap: _ListenerFnType) -> _EventKey[_ET]:
if fn_wrap is self._listen_fn:
return self
else:
return _EventKey(
self.target,
self.identifier,
self.fn,
self.dispatch_target,
_fn_wrap=fn_wrap,
)
def with_dispatch_target(self, dispatch_target: Any) -> _EventKey[_ET]:
if dispatch_target is self.dispatch_target:
return self
else:
return _EventKey(
self.target,
self.identifier,
self.fn,
dispatch_target,
_fn_wrap=self.fn_wrap,
)
def listen(self, *args: Any, **kw: Any) -> None:
once = kw.pop("once", False)
once_unless_exception = kw.pop("_once_unless_exception", False)
named = kw.pop("named", False)
target, identifier, fn = (
self.dispatch_target,
self.identifier,
self._listen_fn,
)
dispatch_collection = getattr(target.dispatch, identifier)
adjusted_fn = dispatch_collection._adjust_fn_spec(fn, named)
self = self.with_wrapper(adjusted_fn)
stub_function = getattr(
self.dispatch_target.dispatch._events, self.identifier
)
if hasattr(stub_function, "_sa_warn"):
stub_function._sa_warn()
if once or once_unless_exception:
self.with_wrapper(
util.only_once(
self._listen_fn, retry_on_exception=once_unless_exception
)
).listen(*args, **kw)
else:
self.dispatch_target.dispatch._listen(self, *args, **kw)
def remove(self) -> None:
key = self._key
if key not in _key_to_collection:
raise exc.InvalidRequestError(
"No listeners found for event %s / %r / %s "
% (self.target, self.identifier, self.fn)
)
dispatch_reg = _key_to_collection.pop(key)
for collection_ref, listener_ref in dispatch_reg.items():
collection = collection_ref()
listener_fn = listener_ref()
if collection is not None and listener_fn is not None:
collection.remove(self.with_wrapper(listener_fn))
def contains(self) -> bool:
"""Return True if this event key is registered to listen."""
return self._key in _key_to_collection
def base_listen(
self,
propagate: bool = False,
insert: bool = False,
named: bool = False,
retval: Optional[bool] = None,
asyncio: bool = False,
) -> None:
target, identifier = self.dispatch_target, self.identifier
dispatch_collection = getattr(target.dispatch, identifier)
for_modify = dispatch_collection.for_modify(target.dispatch)
if asyncio:
for_modify._set_asyncio()
if insert:
for_modify.insert(self, propagate)
else:
for_modify.append(self, propagate)
@property
def _listen_fn(self) -> _ListenerFnType:
return self.fn_wrap or self.fn
def append_to_list(
self,
owner: RefCollection[_ET],
list_: Deque[_ListenerFnType],
) -> bool:
if _stored_in_collection(self, owner):
list_.append(self._listen_fn)
return True
else:
return False
def remove_from_list(
self,
owner: RefCollection[_ET],
list_: Deque[_ListenerFnType],
) -> None:
_removed_from_collection(self, owner)
list_.remove(self._listen_fn)
def prepend_to_list(
self,
owner: RefCollection[_ET],
list_: Deque[_ListenerFnType],
) -> bool:
if _stored_in_collection(self, owner):
list_.appendleft(self._listen_fn)
return True
else:
return False