forked from balisujohn/localwriter
-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathevent_bus.py
More file actions
140 lines (109 loc) · 4.82 KB
/
Copy pathevent_bus.py
File metadata and controls
140 lines (109 loc) · 4.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# WriterAgent - AI Writing Assistant for LibreOffice
# Copyright (c) 2024 John Balis
# Copyright (c) 2026 KeithCu (modifications and relicensing)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Lightweight synchronous event bus for inter-module communication."""
import sys
import logging
import weakref
from plugin.framework.service import ServiceBase
log = logging.getLogger("writeragent.events")
class EventBus:
"""Publish/subscribe event bus.
All callbacks run synchronously on the calling thread. Exceptions in
subscribers are logged but never propagated to the emitter.
Usage::
bus = EventBus()
bus.subscribe("config:changed", my_callback)
bus.emit("config:changed", key="mcp.port", value=9000)
Weak references are supported to avoid preventing garbage collection
of listener objects::
bus.subscribe("document:closed", obj.on_close, weak=True)
"""
def __init__(self):
self._subscribers = {} # event -> list of (callback, is_weakref)
def subscribe(self, event, callback, weak=False):
"""Register *callback* for *event*.
Args:
event: Event name (e.g. "config:changed").
callback: Callable to invoke when the event is emitted.
weak: If True, store a weakref to the callback's bound
object. The subscription auto-removes when the
object is garbage-collected.
"""
if event not in self._subscribers:
self._subscribers[event] = []
if weak and hasattr(callback, "__self__"):
ref = weakref.WeakMethod(callback, lambda r: self._cleanup(event, r))
self._subscribers[event].append((ref, True))
else:
self._subscribers[event].append((callback, False))
def unsubscribe(self, event, callback):
"""Remove *callback* from *event*."""
subs = self._subscribers.get(event)
if not subs:
return
self._subscribers[event] = [(cb, is_weak) for cb, is_weak in subs if self._resolve(cb, is_weak) is not callback]
def emit(self, event, **data):
"""Emit *event*, calling all subscribers with **data as kwargs.
Exceptions in subscribers are logged and swallowed.
"""
subs = self._subscribers.get(event)
if not subs:
return
dead = []
for i, (cb, is_weak) in enumerate(subs):
resolved = self._resolve(cb, is_weak)
if resolved is None:
dead.append(i)
continue
try:
resolved(**data)
except TypeError as e:
log.error("TypeError in event handler %s for %s: %s", resolved, event, e)
except ValueError as e:
log.error("ValueError in event handler %s for %s: %s", resolved, event, e)
except Exception as e:
# Still catch Exception to avoid one bad listener breaking the whole bus,
# but log it clearly as an unhandled application error
log.exception("Unhandled error in event handler %s for %s: %s", resolved, event, e)
# Clean up dead weakrefs
if dead:
for i in reversed(dead):
subs.pop(i)
def _resolve(self, cb, is_weak):
if is_weak:
return cb() # weakref -> call to dereference
return cb
def _cleanup(self, event, ref):
"""Called when a weakref target is garbage-collected."""
subs = self._subscribers.get(event)
if subs:
self._subscribers[event] = [(cb, w) for cb, w in subs if cb is not ref]
def get_event_bus():
"""Return the true singleton EventBus across all LO import contexts."""
if not hasattr(sys, "_writeragent_event_bus"):
setattr(sys, "_writeragent_event_bus", EventBus())
return getattr(sys, "_writeragent_event_bus")
global_event_bus = get_event_bus()
class EventBusService(ServiceBase, EventBus):
"""Singleton event bus exposed as a service.
Inherits from both ServiceBase (for registry) and EventBus (for
pub/sub). Modules access it as ``services.events``.
"""
name = "events"
def __init__(self):
ServiceBase.__init__(self)
self._subscribers = global_event_bus._subscribers