python-library-observer 0.1.0__py3-none-any.whl → 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
observer/bus.py CHANGED
@@ -1,26 +1,81 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import Callable
4
- from threading import RLock
5
- from typing import Any
3
+ import asyncio
4
+ import concurrent.futures
5
+ import inspect
6
+ import logging
7
+ from collections.abc import Awaitable, Callable
8
+ from threading import Event, RLock, Thread
9
+ from typing import Any, TypeVar
6
10
 
7
11
  from .context import ObserverContext
8
12
 
9
13
 
10
- ObserverCallback = Callable[[ObserverContext], None]
14
+ T = TypeVar("T")
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ ObserverSyncCallback = Callable[[ObserverContext], None]
19
+ ObserverAsyncCallback = Callable[[ObserverContext], Awaitable[None]]
20
+ ObserverCallback = Callable[[ObserverContext], object]
11
21
 
12
22
 
13
23
  class ObserverBus:
14
- def __init__(self) -> None:
24
+ """承载订阅、派发与收尾观测总线。
25
+
26
+ 在独立线程池中执行同步回调,在自建事件循环线程上调度协程回调;与业务线程解耦。
27
+ """
28
+
29
+ def __init__(self, *, max_workers: int | None = None) -> None:
30
+ """创建总线并启动后台执行设施。
31
+
32
+ Args:
33
+ max_workers: 同步回调所用线程池的最大工作线程数;未指定时由标准库采用默认策略。
34
+ """
15
35
  self._callbacks: list[tuple[ObserverCallback, dict[str, Any]]] = []
36
+ self._pending: set[concurrent.futures.Future[Any]] = set()
16
37
  self._lock = RLock()
38
+ self._closed = False
39
+
40
+ self._executor = concurrent.futures.ThreadPoolExecutor(
41
+ max_workers=max_workers,
42
+ thread_name_prefix="observer-callback",
43
+ )
44
+
45
+ self._loop = asyncio.new_event_loop()
46
+ self._loop_ready = Event()
47
+ self._loop_thread = Thread(
48
+ target=self._run_event_loop,
49
+ name="observer-async-loop",
50
+ daemon=True,
51
+ )
52
+ self._loop_thread.start()
53
+ self._loop_ready.wait()
17
54
 
18
55
  def subscribe(
19
56
  self,
20
57
  callback: ObserverCallback,
21
58
  **filters: Any,
22
59
  ) -> ObserverCallback:
60
+ """注册监听,在派发上下文与附带条件一致时触发。
61
+
62
+ Args:
63
+ callback: 同步函数、协程函数,或协程可调用实例;入参为当前派发快照。
64
+ **filters: 与快照各槽位逐项相等的匹配条件;留空表示每次派发都考虑该回调。
65
+
66
+ Returns:
67
+ 传入的监听对象,便于链式注册或稍后取消订阅。
68
+
69
+ Raises:
70
+ RuntimeError: 总线已关闭后仍尝试注册。
71
+ ValueError: 匹配条件里出现了快照中不存在的字段名。
72
+ """
73
+ self._validate_filters(filters)
74
+
23
75
  with self._lock:
76
+ if self._closed:
77
+ raise RuntimeError("observer bus is closed")
78
+
24
79
  exists = any(
25
80
  registered_callback is callback and registered_filters == filters
26
81
  for registered_callback, registered_filters in self._callbacks
@@ -30,6 +85,11 @@ class ObserverBus:
30
85
  return callback
31
86
 
32
87
  def unsubscribe(self, callback: ObserverCallback) -> None:
88
+ """按对象身份移除此前注册过的监听,与注册时使用的过滤条件无关。
89
+
90
+ Args:
91
+ callback: 与订阅时传入的同一可调用对象。
92
+ """
33
93
  with self._lock:
34
94
  self._callbacks = [
35
95
  (registered_callback, registered_filters)
@@ -38,6 +98,17 @@ class ObserverBus:
38
98
  ]
39
99
 
40
100
  def callback(self, **filters: Any):
101
+ """返回用于声明式注册监听的装饰器工厂。
102
+
103
+ Args:
104
+ **filters: 与订阅接口相同的匹配条件;非法字段名在装饰器应用时即校验。
105
+
106
+ Returns:
107
+ 接收可调用对象并完成注册的装饰器。
108
+
109
+ Raises:
110
+ ValueError: 匹配条件里出现了快照中不存在的字段名。
111
+ """
41
112
  self._validate_filters(filters)
42
113
 
43
114
  def decorator(fn: ObserverCallback) -> ObserverCallback:
@@ -46,25 +117,36 @@ class ObserverBus:
46
117
  return decorator
47
118
 
48
119
  def emit(self, ctx: ObserverContext) -> None:
120
+ """向当前所有监听异步投递一份上下文快照;总线已关闭时静默忽略。
121
+
122
+ Args:
123
+ ctx: 本次派发携带的快照,监听侧按匹配条件决定是否执行。
124
+ """
49
125
  with self._lock:
126
+ if self._closed:
127
+ return
50
128
  callbacks = tuple(self._callbacks)
51
129
 
52
130
  for callback, filters in callbacks:
53
131
  if not self._match_filters(ctx, filters):
54
132
  continue
55
-
56
- try:
57
- callback(ctx)
58
- except Exception:
59
- # 不要让监听器异常影响原始业务调用
60
- pass
133
+ self._submit_callback(callback, ctx)
61
134
 
62
135
  def observe(
63
136
  self,
64
137
  *,
65
138
  include_private: bool = False,
66
139
  emit_before: bool = True,
67
- ):
140
+ ) -> Callable[[type[T]], type[T]]:
141
+ """返回类装饰器,为被装饰类的方法挂上与当前总线关联的观测包装。
142
+
143
+ Args:
144
+ include_private: 为真时下划线开头的方法也会被包装。
145
+ emit_before: 为真时在目标方法体执行前额外派发切入阶段快照。
146
+
147
+ Returns:
148
+ 接收类型对象并原地改写其可调用成员的类装饰器。
149
+ """
68
150
  from .deractor import observe_methods
69
151
 
70
152
  return observe_methods(
@@ -73,6 +155,129 @@ class ObserverBus:
73
155
  emit_before=emit_before,
74
156
  )
75
157
 
158
+ def close(self, *, wait: bool = True, timeout: float | None = None) -> None:
159
+ """停止接受新订阅并尽量结束在途回调。
160
+
161
+ Args:
162
+ wait: 为真时先等待已提交任务结束再关停执行设施。
163
+ timeout: 等待在途同步任务或后台线程结束的上限秒数;与标准库等待语义一致。
164
+ """
165
+ with self._lock:
166
+ if self._closed:
167
+ return
168
+ self._closed = True
169
+ pending = tuple(self._pending)
170
+
171
+ if wait and pending:
172
+ concurrent.futures.wait(pending, timeout=timeout)
173
+
174
+ self._executor.shutdown(wait=wait, cancel_futures=not wait)
175
+
176
+ if not self._loop.is_closed():
177
+ self._loop.call_soon_threadsafe(self._loop.stop)
178
+
179
+ join_timeout = timeout if timeout is not None else (None if wait else 0.0)
180
+ self._loop_thread.join(join_timeout)
181
+
182
+ def __enter__(self) -> ObserverBus:
183
+ """进入上下文管理作用域时返回自身。"""
184
+ return self
185
+
186
+ def __exit__(self, exc_type, exc, tb) -> None:
187
+ """离开上下文管理作用域时按同步等待策略关闭总线。
188
+
189
+ Args:
190
+ exc_type: 离开作用域时未处理异常的型别;无异常时为 None。
191
+ exc: 异常实例或 None。
192
+ tb: 对应回溯或 None。
193
+ """
194
+ self.close(wait=True)
195
+
196
+ def _submit_callback(self, callback: ObserverCallback, ctx: ObserverContext) -> None:
197
+ if self._is_async_callback(callback):
198
+ future = asyncio.run_coroutine_threadsafe(
199
+ self._run_async_callback(callback, ctx),
200
+ self._loop,
201
+ )
202
+ else:
203
+ future = self._executor.submit(callback, ctx)
204
+
205
+ self._track_future(callback, future)
206
+
207
+ def _track_future(
208
+ self,
209
+ callback: ObserverCallback,
210
+ future: concurrent.futures.Future[Any],
211
+ ) -> None:
212
+ with self._lock:
213
+ if self._closed:
214
+ future.cancel()
215
+ return
216
+ self._pending.add(future)
217
+
218
+ future.add_done_callback(
219
+ lambda done_future: self._on_future_done(callback, done_future)
220
+ )
221
+
222
+ def _on_future_done(
223
+ self,
224
+ callback: ObserverCallback,
225
+ future: concurrent.futures.Future[Any],
226
+ ) -> None:
227
+ with self._lock:
228
+ self._pending.discard(future)
229
+
230
+ try:
231
+ future.result()
232
+ except concurrent.futures.CancelledError:
233
+ return
234
+ except Exception:
235
+ logger.exception(
236
+ "observer callback failed: %s",
237
+ self._callback_name(callback),
238
+ )
239
+
240
+ def _run_event_loop(self) -> None:
241
+ asyncio.set_event_loop(self._loop)
242
+ self._loop_ready.set()
243
+
244
+ try:
245
+ self._loop.run_forever()
246
+ finally:
247
+ pending = asyncio.all_tasks(self._loop)
248
+ for task in pending:
249
+ task.cancel()
250
+
251
+ if pending:
252
+ self._loop.run_until_complete(
253
+ asyncio.gather(*pending, return_exceptions=True)
254
+ )
255
+
256
+ self._loop.run_until_complete(self._loop.shutdown_asyncgens())
257
+ self._loop.close()
258
+
259
+ async def _run_async_callback(
260
+ self,
261
+ callback: ObserverCallback,
262
+ ctx: ObserverContext,
263
+ ) -> None:
264
+ result = callback(ctx)
265
+ if inspect.isawaitable(result):
266
+ await result
267
+
268
+ def _is_async_callback(self, callback: ObserverCallback) -> bool:
269
+ if inspect.iscoroutinefunction(callback):
270
+ return True
271
+
272
+ call = getattr(callback, "__call__", None)
273
+ if call is not None and inspect.iscoroutinefunction(call):
274
+ return True
275
+
276
+ return False
277
+
278
+ def _callback_name(self, callback: ObserverCallback) -> str:
279
+ return getattr(callback, "__qualname__", getattr(callback, "__name__", repr(callback)))
280
+
76
281
  def _match_filters(self, ctx: ObserverContext, filters: dict[str, Any]) -> bool:
77
282
  for key, expected in filters.items():
78
283
  if getattr(ctx, key) != expected:
observer/context.py CHANGED
@@ -10,20 +10,42 @@ ObserverKind = Literal["instance", "class", "static"]
10
10
 
11
11
  @dataclass(slots=True)
12
12
  class ObserverContext:
13
+ """一次方法被观测时,某一时刻要交给监听者的信息包。"""
14
+
15
+ # 标识与归属
13
16
  call_id: str
17
+ """同一次调用共用的编号,before/after/error 对得上"""
14
18
  instance: Any | None
19
+ """实例方法里的 self;不是实例方法则为空"""
15
20
  owner: Any | None
21
+ """类方法里的类;其它情况多为空"""
16
22
  cls: type
23
+ """当作哪个类来记;实例上一般是真实类型"""
17
24
  cls_name: str
25
+ """类名,用来过滤、打印"""
18
26
  method_name: str
27
+ """方法短名,用来过滤"""
19
28
  qualname: str
29
+ """带类前缀的方法名"""
20
30
  method_kind: ObserverKind
31
+ """实例 / 类方法 / 静态"""
32
+ # 载荷
21
33
  args: tuple[Any, ...]
34
+ """位置参数(已去掉 self/cls 这类接收者)"""
22
35
  kwargs: dict[str, Any]
36
+ """关键字参数"""
23
37
  result: Any = None
38
+ """正常返回时的结果;还没执行完可能没有"""
24
39
  error: BaseException | None = None
40
+ """出错时的异常;没出错为空"""
41
+ # 阶段与时间
25
42
  phase: ObserverPhase = "after"
43
+ """before、after、error 三选一"""
26
44
  is_async: bool = False
45
+ """是不是 async 方法"""
27
46
  started_at: float = 0.0
47
+ """开始时刻(性能计数器)"""
28
48
  ended_at: float = 0.0
29
- elapsed: float = 0.0
49
+ """结束时刻;刚开始还没结束可能是 0"""
50
+ elapsed: float = 0.0
51
+ """这段调用花了多少秒"""
observer/deractor.py CHANGED
@@ -5,11 +5,12 @@ import inspect
5
5
  import time
6
6
  import uuid
7
7
  from collections.abc import Callable
8
- from typing import Any
8
+ from typing import Any, TypeVar
9
9
 
10
10
  from .bus import ObserverBus
11
- from .context import ObserverContext, ObserverKind, ObserverPhase
12
11
 
12
+ T = TypeVar("T")
13
+ from .context import ObserverContext, ObserverKind, ObserverPhase
13
14
 
14
15
  _WRAPPED_FLAG = "__observer_bus_wrapped__"
15
16
  _SUBCLASS_HOOK_FLAG = "__observer_bus_subclass_hook_installed__"
@@ -20,8 +21,18 @@ def observe_methods(
20
21
  *,
21
22
  include_private: bool = False,
22
23
  emit_before: bool = True,
23
- ):
24
- def decorator(cls: type) -> type:
24
+ ) -> Callable[[type[T]], type[T]]:
25
+ """返回类装饰器,为指定类型及其子类挂上与总线关联的方法级观测。
26
+
27
+ Args:
28
+ bus: 用于派发各阶段快照的总线实例。
29
+ include_private: 为真时下划线开头的方法也会被包装。
30
+ emit_before: 为真时在目标方法体执行前派发切入阶段快照。
31
+
32
+ Returns:
33
+ 接收类型对象并原地改写其可调用成员的类装饰器。
34
+ """
35
+ def decorator(cls: type[T]) -> type[T]:
25
36
  _observe_class(
26
37
  cls,
27
38
  bus=bus,
@@ -1,4 +1,4 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-library-observer
3
- Version: 0.1.0
3
+ Version: 0.1.3
4
4
  Requires-Python: >=3.10
@@ -0,0 +1,7 @@
1
+ observer/__init__.py,sha256=BQHsdXKbTkLzwH_9Hols36mGACKQaiAN3JcwmbPT3xY,191
2
+ observer/bus.py,sha256=oroVy2Ks50jy7JWd9LMt3ZcX5koMVxaHneuxucDuTJ8,10123
3
+ observer/context.py,sha256=fQ12HGAlAtMWH-GyuHCoUDgCUnmu0xJH7iJRLcmSuwY,1694
4
+ observer/deractor.py,sha256=MTWTHqn1ylQmHzpcLksZUQGFXVtasXQ3ZHfsSr-7ioU,10730
5
+ python_library_observer-0.1.3.dist-info/METADATA,sha256=MoIOlN1S6_zU4equOLXKgGS_TVkAR2gYgSBDIZh9J2E,91
6
+ python_library_observer-0.1.3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
+ python_library_observer-0.1.3.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- observer/__init__.py,sha256=BQHsdXKbTkLzwH_9Hols36mGACKQaiAN3JcwmbPT3xY,191
2
- observer/bus.py,sha256=VqoHzuPPCPPdoT1NwTlTfXMxeaNsU68ZPOu1Y-jnvqA,2784
3
- observer/context.py,sha256=-PmeYuT7wi-SlUjxTOZJMrETJIHsa_3aeEHJz8mzqyI,706
4
- observer/deractor.py,sha256=maBrHvvHlMWb4LgWKzRgpgZKopyXDumV-bX0093Pfa4,10225
5
- python_library_observer-0.1.0.dist-info/METADATA,sha256=pMKdq2oOlxgZYeQ3nURipN45QYou75AOIAJ06AZWBCA,91
6
- python_library_observer-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
7
- python_library_observer-0.1.0.dist-info/RECORD,,