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()