diff --git a/nonebot/__init__.py b/nonebot/__init__.py new file mode 100644 index 00000000..5dc9f792 --- /dev/null +++ b/nonebot/__init__.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import logging +from .log import logger + +logger.setLevel(level=logging.DEBUG) + +from .plugin import load_plugins, get_loaded_plugins diff --git a/nonebot/event.py b/nonebot/event.py index f79d0396..e5e576ac 100644 --- a/nonebot/event.py +++ b/nonebot/event.py @@ -1,4 +1,7 @@ -from typing import Dict, Any, Optional +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from typing import Any, Dict, Optional class Event(dict): diff --git a/nonebot/exception.py b/nonebot/exception.py index de674b15..a0a5ca59 100644 --- a/nonebot/exception.py +++ b/nonebot/exception.py @@ -1,3 +1,7 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + class PausedException(Exception): """Block a message from further handling and try to receive a new message""" pass diff --git a/nonebot/log.py b/nonebot/log.py new file mode 100644 index 00000000..180f1a75 --- /dev/null +++ b/nonebot/log.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +import logging + +logger = logging.getLogger("nonebot") +default_handler = logging.StreamHandler(sys.stdout) +default_handler.setFormatter( + logging.Formatter("[%(asctime)s %(name)s] %(levelname)s: %(message)s")) +logger.addHandler(default_handler) diff --git a/nonebot/matcher.py b/nonebot/matcher.py index c2ed44d7..344fee6a 100644 --- a/nonebot/matcher.py +++ b/nonebot/matcher.py @@ -1,76 +1,70 @@ -import re -import copy +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + from functools import wraps -from typing import Type, Union, Optional, Callable +from collections import defaultdict +from typing import Type, List, Dict, Optional, Callable from .event import Event -from .typing import Scope, Handler -from .rule import Rule, startswith, regex, user +from .typing import Handler +from .rule import Rule, user from .exception import PausedException, RejectedException, FinishedException +matchers: Dict[int, List[Type["Matcher"]]] = defaultdict(list) + class Matcher: rule: Rule = Rule() - scope: Scope = "ALL" - permission: str = "ALL" - block: bool = True - handlers: list = [] + handlers: List[Handler] = [] temp: bool = False + priority: int = 1 _default_state: dict = {} - _default_parser: Optional[Callable[[Event, dict], None]] = None - _args_parser: Optional[Callable[[Event, dict], None]] = None + + # _default_parser: Optional[Callable[[Event, dict], None]] = None + # _args_parser: Optional[Callable[[Event, dict], None]] = None def __init__(self): self.handlers = self.handlers.copy() self.state = self._default_state.copy() - self.parser = self._args_parser or self._default_parser + # self.parser = self._args_parser or self._default_parser @classmethod - def new( - cls, - rule: Rule = Rule(), - scope: Scope = "ALL", - permission: str = "ALL", - block: bool = True, - handlers: list = [], - temp: bool = False, - *, - default_state: dict = {}, - default_parser: Optional[Callable[[Event, dict], None]] = None, - args_parser: Optional[Callable[[Event, dict], None]] = None - ) -> Type["Matcher"]: - - # class NewMatcher(cls): - # rule: Rule = rule - # scope: Scope = scope - # permission: str = permission - # block: bool = block - # handlers: list = handlers - # temp: bool = temp - - # _default_state = default_state + def new(cls, + rule: Rule = Rule(), + handlers: list = [], + temp: bool = False, + priority: int = 1, + *, + default_state: dict = {}) -> Type["Matcher"]: NewMatcher = type( "Matcher", (Matcher,), { "rule": rule, - "scope": scope, - "permission": permission, - "block": block, "handlers": handlers, "temp": temp, - "_default_state": default_state, - "_default_parser": default_parser, - "_args_parser": args_parser, + "priority": priority, + "_default_state": default_state }) + matchers[priority].append(NewMatcher) + return NewMatcher + # @classmethod + # def args_parser(cls, func: Callable[[Event, dict], None]): + # cls._default_parser = func + # return func + @classmethod - def args_parser(cls, func: Callable[[Event, dict], None]): - cls._default_parser = func - return func + def handle(cls): + + def _decorator(func: Handler) -> Handler: + cls.handlers.append(func) + return func + + return _decorator @classmethod def receive(cls): @@ -78,121 +72,78 @@ class Matcher: def _decorator(func: Handler) -> Handler: @wraps(func) - def _handler(event: Event, state: dict): + async def _handler(bot, event: Event, state: dict): raise PausedException cls.handlers.append(_handler) + cls.handlers.append(func) return func return _decorator - @classmethod - def got(cls, - key: str, - prompt: Optional[str] = None, - args_parser: Optional[Callable[[Event, dict], None]] = None): + # @classmethod + # def got(cls, + # key: str, + # prompt: Optional[str] = None, + # args_parser: Optional[Callable[[Event, dict], None]] = None): - def _decorator(func: Handler) -> Handler: + # def _decorator(func: Handler) -> Handler: - @wraps(func) - def _handler(event: Event, state: dict): - if key not in state: - if state.get("__current_arg__", None) == key: - state[key] = event.message - del state["__current_arg__"] - return func(event, state) - state["__current_arg__"] = key - cls._args_parser = args_parser - raise RejectedException + # @wraps(func) + # def _handler(event: Event, state: dict): + # if key not in state: + # if state.get("__current_arg__", None) == key: + # state[key] = event.message + # del state["__current_arg__"] + # return func(event, state) + # state["__current_arg__"] = key + # cls._args_parser = args_parser + # raise RejectedException - return func(event, state) + # return func(event, state) - cls.handlers.append(_handler) + # cls.handlers.append(_handler) - return func + # return func - return _decorator + # return _decorator - @classmethod - def finish(cls, prompt: Optional[str] = None): - raise FinishedException + # @classmethod + # def finish(cls, prompt: Optional[str] = None): + # raise FinishedException - @classmethod - def reject(cls, prompt: Optional[str] = None): - raise RejectedException + # @classmethod + # def reject(cls, prompt: Optional[str] = None): + # raise RejectedException - async def run(self, event): + async def run(self, bot, event): if not self.rule(event): return try: - if self.parser: - await self.parser(event, state) # type: ignore + # if self.parser: + # await self.parser(event, state) # type: ignore for _ in range(len(self.handlers)): handler = self.handlers.pop(0) - await handler(event, self.state) + await handler(bot, event, self.state) except RejectedException: - # TODO: add tmp matcher to matcher tree - self.handlers.insert(handler, 0) - matcher = Matcher.new(self.rule, - self.scope, - self.permission, - self.block, + self.handlers.insert(0, handler) # type: ignore + matcher = Matcher.new(user(event.user_id) & self.rule, self.handlers, temp=True, - default_state=self.state, - default_parser=self._default_parser, - args_parser=self._args_parser) + priority=0, + default_state=self.state) + matchers[0].append(matcher) return except PausedException: - # TODO: add tmp matcher to matcher tree - matcher = Matcher.new(self.rule, - self.scope, - self.permission, - self.block, + matcher = Matcher.new(user(event.user_id) & self.rule, self.handlers, temp=True, - default_state=self.state, - default_parser=self._default_parser, - args_parser=self._args_parser) + priority=0, + default_state=self.state) + matchers[0].append(matcher) return except FinishedException: return - - -def on_message(rule: Rule, - scope="ALL", - permission="ALL", - block=True, - *, - handlers=[], - temp=False, - state={}) -> Type[Matcher]: - # TODO: add matcher to matcher tree - return Matcher.new(rule, - scope, - permission, - block, - handlers=handlers, - temp=temp, - default_state=state) - - -# def on_startswith(msg, -# start: int = None, -# end: int = None, -# rule: Optional[Rule] = None, -# **kwargs) -> Type[Matcher]: -# return on_message(startswith(msg, start, end) & -# rule, **kwargs) if rule else on_message( -# startswith(msg, start, end), **kwargs) - -# def on_regex(pattern, -# flags: Union[int, re.RegexFlag] = 0, -# rule: Optional[Rule] = None, -# **kwargs) -> Type[Matcher]: -# return on_message(regex(pattern, flags) & -# rule, **kwargs) if rule else on_message( -# regex(pattern, flags), **kwargs) diff --git a/nonebot/plugin.py b/nonebot/plugin.py new file mode 100644 index 00000000..7f071425 --- /dev/null +++ b/nonebot/plugin.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import re +import importlib +from types import ModuleType +from typing import Set, Dict, Type, Optional + +from .log import logger +from .rule import Rule +from .matcher import Matcher + +plugins: Dict[str, "Plugin"] = {} + +_tmp_matchers: Set[Type[Matcher]] = set() + + +class Plugin(object): + + # TODO: store plugin informations + def __init__(self, module_path: str, module: ModuleType, + matchers: Set[Type[Matcher]]): + self.module_path = module_path + self.module = module + self.matchers = matchers + + +def on_message(rule: Rule, + *, + handlers=[], + temp=False, + priority: int = 1, + state={}) -> Type[Matcher]: + matcher = Matcher.new(rule, + temp=temp, + priority=priority, + handlers=handlers, + default_state=state) + _tmp_matchers.add(matcher) + return matcher + + +# def on_startswith(msg, +# start: int = None, +# end: int = None, +# rule: Optional[Rule] = None, +# **kwargs) -> Type[Matcher]: +# return on_message(startswith(msg, start, end) & +# rule, **kwargs) if rule else on_message( +# startswith(msg, start, end), **kwargs) + +# def on_regex(pattern, +# flags: Union[int, re.RegexFlag] = 0, +# rule: Optional[Rule] = None, +# **kwargs) -> Type[Matcher]: +# return on_message(regex(pattern, flags) & +# rule, **kwargs) if rule else on_message( +# regex(pattern, flags), **kwargs) + + +def load_plugin(module_path: str) -> Optional[Plugin]: + try: + _tmp_matchers.clear() + module = importlib.import_module(module_path) + plugin = Plugin(module_path, module, _tmp_matchers.copy()) + plugins[module_path] = plugin + logger.info(f"Succeeded to import \"{module_path}\"") + return plugin + except Exception as e: + logger.error(f"Failed to import \"{module_path}\", error: {e}") + logger.exception(e) + return None + + +def load_plugins(plugin_dir: str) -> Set[Plugin]: + plugins = set() + for name in os.listdir(plugin_dir): + path = os.path.join(plugin_dir, name) + if os.path.isfile(path) and \ + (name.startswith("_") or not name.endswith(".py")): + continue + if os.path.isdir(path) and \ + (name.startswith("_") or not os.path.exists( + os.path.join(path, "__init__.py"))): + continue + + m = re.match(r"([_A-Z0-9a-z]+)(.py)?", name) + if not m: + continue + + result = load_plugin(f"{plugin_dir.replace(os.sep, '.')}.{m.group(1)}") + if result: + plugins.add(result) + return plugins + + +def get_loaded_plugins() -> Set[Plugin]: + return set(plugins.values()) diff --git a/nonebot/rule.py b/nonebot/rule.py index 4f18a383..50ac9b5a 100644 --- a/nonebot/rule.py +++ b/nonebot/rule.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + import re from typing import Union, Callable, Optional diff --git a/nonebot/typing.py b/nonebot/typing.py index bb11c609..378ae29e 100644 --- a/nonebot/typing.py +++ b/nonebot/typing.py @@ -1,4 +1,6 @@ -from typing import Literal, Callable +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- -Scope = Literal["PRIVATE", "DISCUSS", "GROUP", "ALL"] -Handler = Callable[["Event", dict], None] +from typing import Callable, Awaitable + +Handler = Callable[["Bot", "Event", dict], Awaitable[None]] diff --git a/tests/bot.py b/tests/bot.py new file mode 100644 index 00000000..8a4eb8e4 --- /dev/null +++ b/tests/bot.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys + +sys.path.insert(0, os.path.abspath(os.path.dirname(os.path.dirname(__file__)))) + +import nonebot +from nonebot.matcher import matchers + +if __name__ == "__main__": + nonebot.load_plugins(os.path.join(os.path.dirname(__file__), + "test_plugins")) + print(nonebot.get_loaded_plugins()) + print(matchers) + print(matchers[1][0].handlers) diff --git a/tests/test_plugins/test_matcher.py b/tests/test_plugins/test_matcher.py new file mode 100644 index 00000000..5a1bb3ea --- /dev/null +++ b/tests/test_plugins/test_matcher.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from nonebot.rule import Rule +from nonebot.event import Event +from nonebot.plugin import on_message + +test_matcher = on_message(Rule(), state={"default": 1}) + + +@test_matcher.handle() +async def test_handler(bot, event: Event, state: dict): + print(state)