Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/sqlalchemy/event/attr.py: 63%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# event/attr.py
2# Copyright (C) 2005-2024 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
8"""Attribute implementation for _Dispatch classes.
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::
15 some_object.dispatch.first_connect(arg1, arg2)
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.
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.
30"""
32from __future__ import absolute_import
33from __future__ import with_statement
35import collections
36from itertools import chain
37import weakref
39from . import legacy
40from . import registry
41from .. import exc
42from .. import util
43from ..util import threading
44from ..util.concurrency import AsyncAdaptedLock
47class RefCollection(util.MemoizedSlots):
48 __slots__ = ("ref",)
50 def _memoized_attr_ref(self):
51 return weakref.ref(self, registry._collection_gced)
54class _empty_collection(object):
55 def append(self, element):
56 pass
58 def extend(self, other):
59 pass
61 def remove(self, element):
62 pass
64 def __iter__(self):
65 return iter([])
67 def clear(self):
68 pass
71class _ClsLevelDispatch(RefCollection):
72 """Class-level events on :class:`._Dispatch` classes."""
74 __slots__ = (
75 "clsname",
76 "name",
77 "arg_names",
78 "has_kw",
79 "legacy_signatures",
80 "_clslevel",
81 "__weakref__",
82 )
84 def __init__(self, parent_dispatch_cls, fn):
85 self.name = fn.__name__
86 self.clsname = parent_dispatch_cls.__name__
87 argspec = util.inspect_getfullargspec(fn)
88 self.arg_names = argspec.args[1:]
89 self.has_kw = bool(argspec.varkw)
90 self.legacy_signatures = list(
91 reversed(
92 sorted(
93 getattr(fn, "_legacy_signatures", []), key=lambda s: s[0]
94 )
95 )
96 )
97 fn.__doc__ = legacy._augment_fn_docs(self, parent_dispatch_cls, fn)
99 self._clslevel = weakref.WeakKeyDictionary()
101 def _adjust_fn_spec(self, fn, named):
102 if named:
103 fn = self._wrap_fn_for_kw(fn)
104 if self.legacy_signatures:
105 try:
106 argspec = util.get_callable_argspec(fn, no_self=True)
107 except TypeError:
108 pass
109 else:
110 fn = legacy._wrap_fn_for_legacy(self, fn, argspec)
111 return fn
113 def _wrap_fn_for_kw(self, fn):
114 def wrap_kw(*args, **kw):
115 argdict = dict(zip(self.arg_names, args))
116 argdict.update(kw)
117 return fn(**argdict)
119 return wrap_kw
121 def _do_insert_or_append(self, event_key, is_append):
122 target = event_key.dispatch_target
123 assert isinstance(
124 target, type
125 ), "Class-level Event targets must be classes."
126 if not getattr(target, "_sa_propagate_class_events", True):
127 raise exc.InvalidRequestError(
128 "Can't assign an event directly to the %s class" % (target,)
129 )
131 for cls in util.walk_subclasses(target):
132 if cls is not target and cls not in self._clslevel:
133 self.update_subclass(cls)
134 else:
135 if cls not in self._clslevel:
136 self.update_subclass(cls)
137 if is_append:
138 self._clslevel[cls].append(event_key._listen_fn)
139 else:
140 self._clslevel[cls].appendleft(event_key._listen_fn)
141 registry._stored_in_collection(event_key, self)
143 def insert(self, event_key, propagate):
144 self._do_insert_or_append(event_key, is_append=False)
146 def append(self, event_key, propagate):
147 self._do_insert_or_append(event_key, is_append=True)
149 def update_subclass(self, target):
150 if target not in self._clslevel:
151 if getattr(target, "_sa_propagate_class_events", True):
152 self._clslevel[target] = collections.deque()
153 else:
154 self._clslevel[target] = _empty_collection()
156 clslevel = self._clslevel[target]
158 for cls in target.__mro__[1:]:
159 if cls in self._clslevel:
160 clslevel.extend(
161 [fn for fn in self._clslevel[cls] if fn not in clslevel]
162 )
164 def remove(self, event_key):
165 target = event_key.dispatch_target
167 for cls in util.walk_subclasses(target):
168 if cls in self._clslevel:
169 self._clslevel[cls].remove(event_key._listen_fn)
170 registry._removed_from_collection(event_key, self)
172 def clear(self):
173 """Clear all class level listeners"""
175 to_clear = set()
176 for dispatcher in self._clslevel.values():
177 to_clear.update(dispatcher)
178 dispatcher.clear()
179 registry._clear(self, to_clear)
181 def for_modify(self, obj):
182 """Return an event collection which can be modified.
184 For _ClsLevelDispatch at the class level of
185 a dispatcher, this returns self.
187 """
188 return self
191class _InstanceLevelDispatch(RefCollection):
192 __slots__ = ()
194 def _adjust_fn_spec(self, fn, named):
195 return self.parent._adjust_fn_spec(fn, named)
198class _EmptyListener(_InstanceLevelDispatch):
199 """Serves as a proxy interface to the events
200 served by a _ClsLevelDispatch, when there are no
201 instance-level events present.
203 Is replaced by _ListenerCollection when instance-level
204 events are added.
206 """
208 propagate = frozenset()
209 listeners = ()
211 __slots__ = "parent", "parent_listeners", "name"
213 def __init__(self, parent, target_cls):
214 if target_cls not in parent._clslevel:
215 parent.update_subclass(target_cls)
216 self.parent = parent # _ClsLevelDispatch
217 self.parent_listeners = parent._clslevel[target_cls]
218 self.name = parent.name
220 def for_modify(self, obj):
221 """Return an event collection which can be modified.
223 For _EmptyListener at the instance level of
224 a dispatcher, this generates a new
225 _ListenerCollection, applies it to the instance,
226 and returns it.
228 """
229 result = _ListenerCollection(self.parent, obj._instance_cls)
230 if getattr(obj, self.name) is self:
231 setattr(obj, self.name, result)
232 else:
233 assert isinstance(getattr(obj, self.name), _JoinedListener)
234 return result
236 def _needs_modify(self, *args, **kw):
237 raise NotImplementedError("need to call for_modify()")
239 exec_once = (
240 exec_once_unless_exception
241 ) = insert = append = remove = clear = _needs_modify
243 def __call__(self, *args, **kw):
244 """Execute this event."""
246 for fn in self.parent_listeners:
247 fn(*args, **kw)
249 def __len__(self):
250 return len(self.parent_listeners)
252 def __iter__(self):
253 return iter(self.parent_listeners)
255 def __bool__(self):
256 return bool(self.parent_listeners)
258 __nonzero__ = __bool__
261class _CompoundListener(_InstanceLevelDispatch):
262 __slots__ = (
263 "_exec_once_mutex",
264 "_exec_once",
265 "_exec_w_sync_once",
266 "_is_asyncio",
267 )
269 def __init__(self, *arg, **kw):
270 super(_CompoundListener, self).__init__(*arg, **kw)
271 self._is_asyncio = False
273 def _set_asyncio(self):
274 self._is_asyncio = True
276 def _memoized_attr__exec_once_mutex(self):
277 if self._is_asyncio:
278 return AsyncAdaptedLock()
279 else:
280 return threading.Lock()
282 def _exec_once_impl(self, retry_on_exception, *args, **kw):
283 with self._exec_once_mutex:
284 if not self._exec_once:
285 try:
286 self(*args, **kw)
287 exception = False
288 except:
289 exception = True
290 raise
291 finally:
292 if not exception or not retry_on_exception:
293 self._exec_once = True
295 def exec_once(self, *args, **kw):
296 """Execute this event, but only if it has not been
297 executed already for this collection."""
299 if not self._exec_once:
300 self._exec_once_impl(False, *args, **kw)
302 def exec_once_unless_exception(self, *args, **kw):
303 """Execute this event, but only if it has not been
304 executed already for this collection, or was called
305 by a previous exec_once_unless_exception call and
306 raised an exception.
308 If exec_once was already called, then this method will never run
309 the callable regardless of whether it raised or not.
311 .. versionadded:: 1.3.8
313 """
314 if not self._exec_once:
315 self._exec_once_impl(True, *args, **kw)
317 def _exec_w_sync_on_first_run(self, *args, **kw):
318 """Execute this event, and use a mutex if it has not been
319 executed already for this collection, or was called
320 by a previous _exec_w_sync_on_first_run call and
321 raised an exception.
323 If _exec_w_sync_on_first_run was already called and didn't raise an
324 exception, then a mutex is not used.
326 .. versionadded:: 1.4.11
328 """
329 if not self._exec_w_sync_once:
330 with self._exec_once_mutex:
331 try:
332 self(*args, **kw)
333 except:
334 raise
335 else:
336 self._exec_w_sync_once = True
337 else:
338 self(*args, **kw)
340 def __call__(self, *args, **kw):
341 """Execute this event."""
343 for fn in self.parent_listeners:
344 fn(*args, **kw)
345 for fn in self.listeners:
346 fn(*args, **kw)
348 def __len__(self):
349 return len(self.parent_listeners) + len(self.listeners)
351 def __iter__(self):
352 return chain(self.parent_listeners, self.listeners)
354 def __bool__(self):
355 return bool(self.listeners or self.parent_listeners)
357 __nonzero__ = __bool__
360class _ListenerCollection(_CompoundListener):
361 """Instance-level attributes on instances of :class:`._Dispatch`.
363 Represents a collection of listeners.
365 As of 0.7.9, _ListenerCollection is only first
366 created via the _EmptyListener.for_modify() method.
368 """
370 __slots__ = (
371 "parent_listeners",
372 "parent",
373 "name",
374 "listeners",
375 "propagate",
376 "__weakref__",
377 )
379 def __init__(self, parent, target_cls):
380 super(_ListenerCollection, self).__init__()
381 if target_cls not in parent._clslevel:
382 parent.update_subclass(target_cls)
383 self._exec_once = False
384 self._exec_w_sync_once = False
385 self.parent_listeners = parent._clslevel[target_cls]
386 self.parent = parent
387 self.name = parent.name
388 self.listeners = collections.deque()
389 self.propagate = set()
391 def for_modify(self, obj):
392 """Return an event collection which can be modified.
394 For _ListenerCollection at the instance level of
395 a dispatcher, this returns self.
397 """
398 return self
400 def _update(self, other, only_propagate=True):
401 """Populate from the listeners in another :class:`_Dispatch`
402 object."""
404 existing_listeners = self.listeners
405 existing_listener_set = set(existing_listeners)
406 self.propagate.update(other.propagate)
407 other_listeners = [
408 l
409 for l in other.listeners
410 if l not in existing_listener_set
411 and not only_propagate
412 or l in self.propagate
413 ]
415 existing_listeners.extend(other_listeners)
417 if other._is_asyncio:
418 self._set_asyncio()
420 to_associate = other.propagate.union(other_listeners)
421 registry._stored_in_collection_multi(self, other, to_associate)
423 def insert(self, event_key, propagate):
424 if event_key.prepend_to_list(self, self.listeners):
425 if propagate:
426 self.propagate.add(event_key._listen_fn)
428 def append(self, event_key, propagate):
429 if event_key.append_to_list(self, self.listeners):
430 if propagate:
431 self.propagate.add(event_key._listen_fn)
433 def remove(self, event_key):
434 self.listeners.remove(event_key._listen_fn)
435 self.propagate.discard(event_key._listen_fn)
436 registry._removed_from_collection(event_key, self)
438 def clear(self):
439 registry._clear(self, self.listeners)
440 self.propagate.clear()
441 self.listeners.clear()
444class _JoinedListener(_CompoundListener):
445 __slots__ = "parent", "name", "local", "parent_listeners"
447 def __init__(self, parent, name, local):
448 self._exec_once = False
449 self.parent = parent
450 self.name = name
451 self.local = local
452 self.parent_listeners = self.local
454 @property
455 def listeners(self):
456 return getattr(self.parent, self.name)
458 def _adjust_fn_spec(self, fn, named):
459 return self.local._adjust_fn_spec(fn, named)
461 def for_modify(self, obj):
462 self.local = self.parent_listeners = self.local.for_modify(obj)
463 return self
465 def insert(self, event_key, propagate):
466 self.local.insert(event_key, propagate)
468 def append(self, event_key, propagate):
469 self.local.append(event_key, propagate)
471 def remove(self, event_key):
472 self.local.remove(event_key)
474 def clear(self):
475 raise NotImplementedError()