python-library-callback 0.1.1__tar.gz

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.
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ .env
9
+ .pytest_cache/
10
+ config.yaml
11
+ logs/
@@ -0,0 +1,4 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-library-callback
3
+ Version: 0.1.1
4
+ Requires-Python: >=3.10
@@ -0,0 +1,176 @@
1
+ from __future__ import annotations
2
+ from typing import ClassVar, Callable, TypeVar
3
+ import asyncio
4
+ import logging
5
+ import inspect
6
+ from concurrent.futures import ThreadPoolExecutor
7
+
8
+ logger = logging.getLogger(__name__)
9
+ T = TypeVar("T", bound="Callback")
10
+
11
+ class Callback():
12
+ """
13
+ 回调
14
+
15
+ 定义回调结构时:
16
+ class A(Callback):
17
+ attr: type
18
+
19
+ 触发回调时:
20
+ cb = A.trigger(attr=value) # 同步
21
+ cb = await A.atrigger(attr=value) # 异步
22
+
23
+ 为函数注册回调时:
24
+ @A
25
+ def func(cb:A):
26
+ pass
27
+ """
28
+ function_registry: ClassVar[dict[str, list[Callable]]] = {}
29
+ """注册的函数列表"""
30
+ _async: ClassVar[bool] = False
31
+ """是否异步"""
32
+
33
+ def __new__(cls, *args, **kwargs):
34
+ """支持把 Callback 子类直接当装饰器使用"""
35
+ if len(args) == 1 and not kwargs and callable(args[0]):
36
+ func = args[0]
37
+ if cls._async != asyncio.iscoroutinefunction(func):
38
+ raise ValueError(
39
+ f"函数{func}是{'异步' if asyncio.iscoroutinefunction(func) else '同步'},"
40
+ f"但回调{cls.__name__}是{'异步' if cls._async else '同步'}"
41
+ )
42
+ cls.register(func)
43
+ return func
44
+ return super().__new__(cls)
45
+
46
+ def __init__(self, *args, **kwargs):
47
+ """初始化事件实例"""
48
+ field_names = list(self.__class__.__annotations__.keys())
49
+ for i, arg in enumerate(args):
50
+ if i < len(field_names):
51
+ setattr(self, field_names[i], arg)
52
+ else:
53
+ raise ValueError(f"参数过多[{i}]: {arg}")
54
+ for key, value in kwargs.items():
55
+ if key in field_names:
56
+ setattr(self, key, value)
57
+ else:
58
+ raise ValueError(f"未知属性[{key}]: {value}")
59
+
60
+ @classmethod
61
+ def register(cls, func: Callable):
62
+ """注册函数"""
63
+ try:
64
+ if cls.__name__ not in cls.function_registry:
65
+ cls.function_registry[cls.__name__] = []
66
+ if func not in cls.function_registry[cls.__name__]:
67
+ cls.function_registry[cls.__name__].append(func)
68
+ except Exception as e:
69
+ logger.exception(f"注册函数{func}失败: {e}")
70
+ raise e
71
+
72
+ @classmethod
73
+ def trigger(cls:type[T],*args,**kwargs) -> T:
74
+ """同步触发回调"""
75
+ try:
76
+ self = cls(*args, **kwargs)
77
+
78
+ self.before_trigger()
79
+ funcs = cls.function_registry.get(cls.__name__, [])
80
+ with ThreadPoolExecutor() as executor:
81
+ futures = [executor.submit(self._call_registered, func, self) for func in funcs]
82
+ for future in futures:
83
+ future.result()
84
+ self.after_trigger()
85
+
86
+ return self
87
+ except Exception as e:
88
+ logger.exception(f"触发回调{cls}失败: {e}")
89
+ raise e
90
+
91
+ @classmethod
92
+ async def atrigger(cls:type[T],*args,**kwargs) -> T:
93
+ """异步触发回调"""
94
+ try:
95
+ self = cls(*args, **kwargs)
96
+
97
+ await self.before_atrigger()
98
+ funcs = cls.function_registry.get(cls.__name__, [])
99
+ tasks = [self._acall_registered(func, self) for func in funcs]
100
+ if tasks:
101
+ await asyncio.gather(*tasks)
102
+ await self.after_atrigger()
103
+
104
+ return self
105
+ except Exception as e:
106
+ logger.exception(f"异步触发回调{cls}失败: {e}")
107
+ raise e
108
+
109
+ def before_trigger(self) -> None:
110
+ """同步触发前钩子"""
111
+ pass
112
+
113
+ async def before_atrigger(self) -> None:
114
+ """异步触发前钩子"""
115
+ pass
116
+
117
+ def after_trigger(self) -> None:
118
+ """同步触发后钩子"""
119
+ pass
120
+
121
+ async def after_atrigger(self) -> None:
122
+ """异步触发后钩子"""
123
+ pass
124
+
125
+ @staticmethod
126
+ def _call_registered(func: Callable, cb: "Callback"):
127
+ """同步调用注册的函数,支持不传参数"""
128
+ try:
129
+ sig = inspect.signature(func)
130
+ except (TypeError, ValueError):
131
+ return func(cb)
132
+
133
+ params = list(sig.parameters.values())
134
+ positional = [
135
+ p for p in params
136
+ if p.kind in (
137
+ inspect.Parameter.POSITIONAL_ONLY,
138
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
139
+ )
140
+ ]
141
+ has_varargs = any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in params)
142
+
143
+ if has_varargs or len(positional) >= 1:
144
+ return func(cb)
145
+ return func()
146
+
147
+ @staticmethod
148
+ async def _acall_registered(func: Callable, cb: "Callback"):
149
+ """异步调用注册的函数,支持不传参数"""
150
+ try:
151
+ sig = inspect.signature(func)
152
+ except (TypeError, ValueError):
153
+ return await func(cb)
154
+
155
+ params = list(sig.parameters.values())
156
+ positional = [
157
+ p for p in params
158
+ if p.kind in (
159
+ inspect.Parameter.POSITIONAL_ONLY,
160
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
161
+ )
162
+ ]
163
+ has_varargs = any(p.kind == inspect.Parameter.VAR_POSITIONAL for p in params)
164
+
165
+ if has_varargs or len(positional) >= 1:
166
+ return await func(cb)
167
+ return await func()
168
+
169
+ @classmethod
170
+ def get_all(cls) -> list[type[Callback]]:
171
+ """获取所有回调"""
172
+ return list(cls.__subclasses__())
173
+
174
+ __all__ = [
175
+ "Callback",
176
+ ]
@@ -0,0 +1,12 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "python-library-callback"
7
+ version = "0.1.1"
8
+ requires-python = ">=3.10"
9
+ dependencies = []
10
+
11
+ [tool.hatch.build.targets.wheel]
12
+ packages = ["callback"]
@@ -0,0 +1,10 @@
1
+ @echo off
2
+ cd /d %~dp0
3
+
4
+ if not exist .venv (
5
+ python -m venv .venv
6
+ )
7
+
8
+ call .venv\Scripts\activate.bat
9
+ python -m pip install -e .
10
+ python -m unittest discover -s tests -p "test_*.py"
@@ -0,0 +1,28 @@
1
+ import unittest
2
+
3
+ from callback import Callback
4
+
5
+
6
+ class CallbackTests(unittest.TestCase):
7
+ def tearDown(self) -> None:
8
+ Callback.function_registry.clear()
9
+
10
+ def test_trigger_invokes_registered_handler(self) -> None:
11
+ class Inc(Callback):
12
+ callback_name: str = "Inc"
13
+ name: str
14
+ data: int
15
+
16
+ class Holder:
17
+ def __init__(self) -> None:
18
+ @Inc
19
+ def _(cb: Inc) -> None:
20
+ cb.data += 1
21
+
22
+ Holder()
23
+ cb = Inc.trigger(name="test", data=1)
24
+ self.assertEqual(cb.data, 2)
25
+
26
+
27
+ if __name__ == "__main__":
28
+ unittest.main()