Skip to content

Commit

Permalink
✨ version 0.6.0
Browse files Browse the repository at this point in the history
add `deref` to build predicate
  • Loading branch information
RF-Tar-Railt committed May 23, 2023
1 parent 8c00eca commit 7b98bd9
Show file tree
Hide file tree
Showing 14 changed files with 219 additions and 100 deletions.
58 changes: 52 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ es = EventSystem()


class TestEvent:

async def gather(self, context: Contexts):
context["name"] = "Letoderea"

Expand All @@ -37,11 +36,58 @@ es.loop.run_until_complete(es.publish(TestEvent()))

## 特性

- 任何实现了 `gather` 方法的类都可以作为事件
- 通过 `Provider` 实现依赖注入的静态绑定,提高性能
- 通过 `Contexts` 增强事件传递的灵活性
- 通过 `Publisher` 实现事件响应的隔离
- 通过 `Auxiliary` 提供了一系列辅助方法,方便事件的处理
### 事件

- 事件可以是任何对象,只要实现了 `gather` 异步方法
- `gather` 方法的参数为 `Contexts` 类型,用于传递上下文信息
- 事件可以通过 `gather` 方法将自身想要传递的信息整合进 `Contexts`
- 事件系统支持直接查找属性, 例如 `Event.name` 可以直接注入进 `foo(name: str)` 的参数中
- 事件可以携带 `Provider``Auxiliary`,它们会在事件被订阅时注入到订阅者中

### 订阅

- 通过 `EventSystem.on``subscribe` 装饰器可以将一个函数注册为事件的订阅者
- 订阅者的参数可以是任何类型,事件系统会尝试从 `Contexts` 中查找对应的值并注入
- 订阅者的参数可以是 `Contexts` 类型,用于获取事件的上下文信息
- 默认情况下 `event` 为名字的参数会被注入为事件的实例

### 上下文

- `Contexts` 类型是一个 `dict` 的子类,用于传递上下文信息
- `Contexts` 默认包含 `event` 键,其值为事件的实例
- `Contexts` 默认包含 `$subscriber` 键,其值为订阅者的实例

### 发布

- 通过 `EventSystem.publish` 方法可以发布一个事件
- `Publisher` 负责管理订阅者与事件的交互
- `Publisher.validate` 方法用于验证该事件是否为该发布者的订阅者所关注的事件
- `Publisher.publish` 方法用于将事件主动分发给订阅者
- `Publisher.supply` 方法用于给事件系统提供可能的事件
- `EventSystem.on``EventSystem.publish` 可以指定 `Publisher`,默认为事件系统内的全局 `Publisher`

### 参数

- `Provider[T]` 负责管理参数的注入, 其会尝试从 `Contexts` 中选择需求的参数返回
- `Provider.validate` 方法用于验证订阅函数的参数是否为该 `Provider` 所关注的参数
- `Provider.__call__` 方法用于从 `Contexts` 中获取参数

### 辅助

- `Auxiliary` 提供了一系列辅助方法,方便事件的处理
- `Auxiliary` 分为 `Judge`, `Supply``Depend` 三类:
- `Judge`: 用于判断此时是否应该处理事件
- `Supply`: 用于为 `Contexts` 提供额外的信息
- `Depend`: 用于依赖注入
- `Auxiliary.scopes` 声明了 `Auxiliary` 的作用域:
- `prepare`: 表示该 `Auxiliary` 会在依赖注入之前执行
- `parsing`: 表示该 `Auxiliary` 会在依赖注入解析时执行
- `complete`: 表示该 `Auxiliary` 会在依赖注入完成后执行
- `cleanup`: 表示该 `Auxiliary` 会在事件处理完成后执行
- `Auxiliary` 可以设置 `CombineMode`, 用来设置多个 `Auxiliary` 的组合方式:
- `single`: 表示该 `Auxiliary` 独立执行
- `and`: 表示该 `Auxiliary` 的执行结果应该与其他 `Auxiliary` 的执行结果都为有效值
- `or`: 表示该 `Auxiliary` 的执行结果应该与其他 `Auxiliary` 的执行结果至少有一个为有效值

## 开源协议
本实现以 MIT 为开源协议。
1 change: 1 addition & 0 deletions arclet/letoderea/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@
from .provider import Param, Provider, provide
from .publisher import Publisher
from .typing import Contexts, Force
from .ref import deref
3 changes: 1 addition & 2 deletions arclet/letoderea/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def add_publisher(self, publisher: Publisher):
self.publishers[publisher.id] = publisher

def publish(self, event: BaseEvent, publisher: str | Publisher | None = None):
pubs = []
pubs = [self._backend_publisher]
if isinstance(publisher, str) and (pub := self.publishers.get(publisher)):
pubs.append(pub)
elif not publisher:
Expand All @@ -101,7 +101,6 @@ def publish(self, event: BaseEvent, publisher: str | Publisher | None = None):
)
else:
pubs.append(publisher)
pubs.append(self._backend_publisher)
subscribers = sum((pub.subscribers.get(event.__class__, []) for pub in pubs), [])
task = self.loop.create_task(dispatch(subscribers, event))
task.add_done_callback(self._ref_tasks.discard)
Expand Down
39 changes: 23 additions & 16 deletions arclet/letoderea/decorate.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Callable, Type, TypeVar, Union, Optional
from typing import Callable, Type, TypeVar, Union, Optional, TYPE_CHECKING

from . import Scope
from .auxiliary import BaseAuxiliary, AuxType, auxilia
Expand All @@ -8,6 +8,7 @@
from .subscriber import Subscriber, _compile
from .typing import Contexts
from .exceptions import ParsingStop
from .ref import generate, Deref

TWrap = TypeVar("TWrap", bound=Union[Callable, Subscriber])

Expand Down Expand Up @@ -50,22 +51,28 @@ def wrapper(target: TWrap) -> TWrap:
return wrapper


def bypass_if(predicate: Callable[[Contexts], bool]):
def _prepare(context: Contexts) -> Optional[bool]:
if predicate(context):
raise ParsingStop()
return True
if TYPE_CHECKING:
def bypass_if(predicate: Union[Callable[[Contexts], bool], bool]):
...
else:
def bypass_if(predicate: Union[Callable[[Contexts], bool], Deref]):
_predicate = generate(predicate) if isinstance(predicate, Deref) else predicate

inner = auxilia(AuxType.judge, prepare=_prepare)
def _prepare(context: Contexts) -> Optional[bool]:
if _predicate(context):
raise ParsingStop()
return True

def wrapper(target: TWrap) -> TWrap:
if isinstance(target, Subscriber):
target.auxiliaries[Scope.prepare].append(inner)
else:
if not hasattr(target, "__auxiliaries__"):
setattr(target, "__auxiliaries__", [inner])
inner = auxilia(AuxType.judge, prepare=_prepare)

def wrapper(target: TWrap) -> TWrap:
if isinstance(target, Subscriber):
target.auxiliaries[Scope.prepare].append(inner)
else:
getattr(target, "__auxiliaries__").append(inner)
return target
if not hasattr(target, "__auxiliaries__"):
setattr(target, "__auxiliaries__", [inner])
else:
getattr(target, "__auxiliaries__").append(inner)
return target

return wrapper
return wrapper
8 changes: 6 additions & 2 deletions arclet/letoderea/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,15 @@ async def param_parser(
if annotation:
for key, value in context.items():
if generic_isinstance(value, annotation):
providers.append(provide(annotation, target=key)())
providers.append(provide(annotation, key)())
return value
if isinstance(annotation, str) and f"{type(value)}" == annotation:
providers.append(provide(type(value), target=key)())
providers.append(provide(type(value), key)())
return value
if hasattr(context["event"], name):
value = getattr(context["event"], name)
providers.append(provide(type(value), call=lambda x: getattr(x['event'], name))())
return value
if default is not Empty:
return default
raise UndefinedRequirement(name, annotation, default, providers)
16 changes: 9 additions & 7 deletions arclet/letoderea/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ async def __call__(self, context: Contexts) -> T | None:

def provide(
origin: type[T],
name: str = "_Provider",
call: Callable[[Contexts], T | None | Awaitable[T | None]] | None = None,
validate: Callable[[Param], bool] | None = None,
target: str | None = None,
call: Callable[[Contexts], T | None | Awaitable[T | None]] | str | None = None,
validate: Callable[[Param], bool] | None = None,
_id: str = "_Provider",
) -> type[Provider[T]]:
"""
用于动态生成 Provider 的装饰器
Expand All @@ -81,11 +81,13 @@ def validate(self, param: Param):
)

async def __call__(self, context: Contexts):
return (
await run_always_await(call, context) if call else context.get(target)
)
if not call:
return context.get(target)
if isinstance(call, str):
return context.get(call)
return await run_always_await(call, context)

def __repr__(self):
return f"Provider::{name}(origin={origin})"
return f"Provider::{_id}(origin={origin})"

return _Provider
100 changes: 100 additions & 0 deletions arclet/letoderea/ref.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from typing import Generic, TypeVar, Callable, Any
from .typing import Contexts

T = TypeVar("T")


class Deref(Generic[T]):
def __init__(self, proxy_type: type[T]):
self.__proxy_type = proxy_type
self.__items = {}
self.__last_key = None

def __getattr__(self, item):
self.__items[item] = None
self.__last_key = item
return self

def __call__(self, *args, **kwargs):
if not self.__items:
return self.__proxy_type(*args, **kwargs)
self.__items[self.__last_key] = ("call", lambda x: x(*args, **kwargs))
return self

def __eq__(self, other):
self.__items[self.__last_key] = lambda x: x == other
return self

def __ne__(self, other):
self.__items[self.__last_key] = lambda x: x != other
return self

def __lt__(self, other):
self.__items[self.__last_key] = lambda x: x < other
return self

def __gt__(self, other):
self.__items[self.__last_key] = lambda x: x > other
return self

def __le__(self, other):
self.__items[self.__last_key] = lambda x: x <= other
return self

def __ge__(self, other):
self.__items[self.__last_key] = lambda x: x >= other
return self

def __contains__(self, item):
self.__items[self.__last_key] = lambda x: item in x
return self

def __getitem__(self, item):
self.__items[self.__last_key] = ("getitem", lambda x: x[item])
return self

def __or__(self, other):
self.__items[self.__last_key] = lambda x: x | other
return self

def __and__(self, other):
self.__items[self.__last_key] = lambda x: x & other
return self

def __xor__(self, other):
self.__items[self.__last_key] = lambda x: x ^ other
return self

def __repr__(self):
return repr(self.__items)

def __iter__(self):
return iter(self.__items.items())

def __len__(self):
return len(self.__items)


def generate(ref: Deref) -> Callable[[Contexts], Any]:
if len(ref) == 0:
return lambda x: x["event"]

def _get(ctx: Contexts):
item = ctx["event"]
for key, value in ref:
if not (item := getattr(item, key, ctx.get(key, None))):
return item
if isinstance(value, tuple):
if value[0] == "call":
item = value[1](item)
elif value[0] == "getitem":
item = value[1](item)
elif callable(value):
return value(item)
return item

return _get


def deref(proxy_type: type[T]) -> T: # type: ignore
return Deref(proxy_type) # type: ignore
3 changes: 3 additions & 0 deletions arclet/letoderea/subscriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .auxiliary import Scope, AuxType, BaseAuxiliary, Executor, combine
from .provider import Param, Provider, provide
from .typing import TTarget
from .ref import Deref, generate


@dataclass
Expand Down Expand Up @@ -38,6 +39,8 @@ def _compile(target: Callable, providers: list[Provider]) -> list[CompileParam]:
param.providers.insert(0, m)
elif isinstance(m, str):
param.providers.insert(0, provide(org, name, lambda x: x.get(m))())
elif isinstance(m, Deref):
param.providers.insert(0, provide(org, name, generate(m))())
elif callable(m):
param.providers.insert(0, provide(org, name, m)())
if isinstance(default, Provider):
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "arclet-letoderea"
version = "0.5.0"
version = "0.6.0"
description = "A high-performance, simple-structured event system, relies on asyncio"
authors = [
{name = "RF-Tar-Railt", email = "rf_tar_railt@qq.com"},
Expand Down
29 changes: 5 additions & 24 deletions tests/annotated.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from arclet.letoderea import provide, EventSystem
from typing import TypeVar, Generic, TYPE_CHECKING
from arclet.letoderea import EventSystem
from arclet.letoderea.ref import deref
from typing_extensions import Annotated

es = EventSystem()
T = TypeVar("T")


class TestEvent:
Expand All @@ -14,24 +13,6 @@ async def gather(self, context: dict):
context['index'] = self.index


class Deref(Generic[T]):
def __init__(self, proxy_type: type[T]):
self.proxy_type = proxy_type

def __getattr__(self, item):
if item not in self.proxy_type.__annotations__:
raise AttributeError(f"{self.proxy_type.__name__} has no attribute {item}")
return lambda x: x.get(item)


if TYPE_CHECKING:
def deref(proxy_type: type[T]) -> T:
...
else:
def deref(proxy_type: type[T]):
return Deref(proxy_type)


@es.on(TestEvent)
async def test(
index: Annotated[int, "index"],
Expand All @@ -45,15 +26,15 @@ async def test1(
index: Annotated[int, lambda x: x['index']],
a: str = "hello"
):
print("test:", index, a)
print("test1:", index, a)


@es.on(TestEvent)
async def test1(
async def test2(
index: Annotated[int, deref(TestEvent).index],
a: str = "hello"
):
print("test:", index, a)
print("test2:", index, a)


es.loop.run_until_complete(es.publish(TestEvent()))
6 changes: 3 additions & 3 deletions tests/except.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ async def gather(self, context: Contexts):


@subscribe(TestEvent)
@bind(provide(int, "foo", lambda x: x.get('a')))
@bind(provide(int, "bar", lambda x: x.get('b')))
@bind(provide(int, "baz", lambda x: x.get('c')))
@bind(provide(int, "age", 'a', _id="foo"))
@bind(provide(int, "age", 'b', _id="bar"))
@bind(provide(int, "age", 'c', _id="baz"))
async def test_subscriber(name: str, age: int):
print(name, age)

Expand Down
Loading

0 comments on commit 7b98bd9

Please sign in to comment.