From 1f09023f66fca6b595e1035d9ebc5d62305cc99b Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Wed, 26 Aug 2020 22:54:58 +0800 Subject: [PATCH 1/6] :loud_sound: change logger into loguru --- docs/api/log.md | 4 ++-- nonebot/log.py | 29 ++++++++++++++++++---------- poetry.lock | 51 ++++++++++++++++++++++++++++++++++++++++++++++--- pyproject.toml | 5 +++-- 4 files changed, 72 insertions(+), 17 deletions(-) diff --git a/docs/api/log.md b/docs/api/log.md index ecc3bdc3..fdc24b50 100644 --- a/docs/api/log.md +++ b/docs/api/log.md @@ -7,9 +7,9 @@ sidebarDepth: 0 ## 日志 -NoneBot 使用标准库 [logging](https://docs.python.org/3/library/logging.html) 来记录日志信息。 +NoneBot 使用 [loguru](https://github.com/Delgan/loguru) 来记录日志信息。 -自定义 logger 请参考 [logging](https://docs.python.org/3/library/logging.html) 文档。 +自定义 logger 请参考 [loguru](https://github.com/Delgan/loguru) 文档。 ## `logger` diff --git a/nonebot/log.py b/nonebot/log.py index ae03d2f3..8218ca57 100644 --- a/nonebot/log.py +++ b/nonebot/log.py @@ -4,18 +4,21 @@ 日志 ==== -NoneBot 使用标准库 `logging`_ 来记录日志信息。 +NoneBot 使用 `loguru`_ 来记录日志信息。 -自定义 logger 请参考 `logging`_ 文档。 +自定义 logger 请参考 `loguru`_ 文档。 -.. _logging: - https://docs.python.org/3/library/logging.html +.. _loguru: + https://github.com/Delgan/loguru """ import sys -import logging +# import logging -logger = logging.getLogger("nonebot") +from loguru import logger as logger_ + +# logger = logging.getLogger("nonebot") +logger = logger_ """ :说明: @@ -38,7 +41,13 @@ logger = logging.getLogger("nonebot") 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) +# default_handler = logging.StreamHandler(sys.stdout) +# default_handler.setFormatter( +# logging.Formatter("[%(asctime)s %(name)s] %(levelname)s: %(message)s")) +# logger.addHandler(default_handler) + +logger.remove() +default_format = ("{level: <8}|" + "{time:MM-DD HH:mm:ss}|" + "{message}") +logger.add(sys.stdout, colorize=True, format=default_format) diff --git a/poetry.lock b/poetry.lock index b7a98f4e..5588003d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -99,7 +99,7 @@ type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" [[package]] -category = "dev" +category = "main" description = "Cross-platform colored terminal text." marker = "sys_platform == \"win32\"" name = "colorama" @@ -332,6 +332,26 @@ reference = "aliyun" type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" +[[package]] +category = "main" +description = "Python logging made (stupidly) simple" +name = "loguru" +optional = false +python-versions = ">=3.5" +version = "0.5.1" + +[package.dependencies] +colorama = ">=0.3.4" +win32-setctime = ">=1.0.0" + +[package.extras] +dev = ["codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "isort (>=4.3.20)", "tox (>=3.9.0)", "tox-travis (>=0.12)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "Sphinx (>=2.2.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "black (>=19.3b0)"] + +[package.source] +reference = "aliyun" +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" + [[package]] category = "dev" description = "Safely add untrusted strings to HTML/XML markup." @@ -603,6 +623,7 @@ yapf = "*" reference = "1438d33cbeaab0230c9f7e33bd059eb9f57c86d6" type = "git" url = "https://github.com/nonebot/sphinx-markdown-builder.git" + [[package]] category = "dev" description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" @@ -833,6 +854,23 @@ reference = "aliyun" type = "legacy" url = "https://mirrors.aliyun.com/pypi/simple" +[[package]] +category = "main" +description = "A small Python utility to set file creation time on Windows" +marker = "sys_platform == \"win32\"" +name = "win32-setctime" +optional = false +python-versions = ">=3.5" +version = "1.0.1" + +[package.extras] +dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] + +[package.source] +reference = "aliyun" +type = "legacy" +url = "https://mirrors.aliyun.com/pypi/simple" + [[package]] category = "dev" description = "A formatter for Python code." @@ -848,10 +886,9 @@ url = "https://mirrors.aliyun.com/pypi/simple" [extras] scheduler = ["apscheduler"] -test = [] [metadata] -content-hash = "4d16d7ad0930bc9851802bc149f843c4e990a987e89414d765579ea8dccc8d6e" +content-hash = "2e8f1fc9fcb89a528ecbebbf0f2315abf39e3de8eb40c133b91085a784e49173" python-versions = "^3.7" [metadata.files] @@ -949,6 +986,10 @@ jinja2 = [ {file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"}, {file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"}, ] +loguru = [ + {file = "loguru-0.5.1-py3-none-any.whl", hash = "sha256:e5d362a43cd2fc2da63551d79a6830619c4d5b3a8b976515748026f92f351b61"}, + {file = "loguru-0.5.1.tar.gz", hash = "sha256:70201d5fce26da89b7a5f168caa2bb674e06b969829f56737db1d6472e53e7c3"}, +] markupsafe = [ {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, @@ -1136,6 +1177,10 @@ websockets = [ {file = "websockets-8.1-cp38-cp38-win_amd64.whl", hash = "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"}, {file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"}, ] +win32-setctime = [ + {file = "win32_setctime-1.0.1-py3-none-any.whl", hash = "sha256:568fd636c68350bcc54755213fe01966fe0a6c90b386c0776425944a0382abef"}, + {file = "win32_setctime-1.0.1.tar.gz", hash = "sha256:b47e5023ec7f0b4962950902b15bc56464a380d869f59d27dbf9ab423b23e8f9"}, +] yapf = [ {file = "yapf-0.30.0-py2.py3-none-any.whl", hash = "sha256:3abf61ba67cf603069710d30acbc88cfe565d907e16ad81429ae90ce9651e0c9"}, {file = "yapf-0.30.0.tar.gz", hash = "sha256:3000abee4c28daebad55da6c85f3cd07b8062ce48e2e9943c8da1b9667d48427"}, diff --git a/pyproject.toml b/pyproject.toml index 5f222f53..9c3f8e41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,15 +27,16 @@ fastapi = "^0.58.1" uvicorn = "^0.11.5" pydantic = { extras = ["dotenv"], version = "^1.6.1" } apscheduler = { version = "^3.6.3", optional = true } -nonebot-test = { version = "^0.1.0", optional = true } +# nonebot-test = { version = "^0.1.0", optional = true } +loguru = "^0.5.1" [tool.poetry.dev-dependencies] yapf = "^0.30.0" sphinx = "^3.1.1" sphinx-markdown-builder = { git = "https://github.com/nonebot/sphinx-markdown-builder.git" } [tool.poetry.extras] -test = ["nonebot-test"] +# test = ["nonebot-test"] scheduler = ["apscheduler"] [[tool.poetry.source]] From 858098b9467fa5f0d8bf3daa54651670963b7b20 Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Thu, 27 Aug 2020 13:27:42 +0800 Subject: [PATCH 2/6] :loud_sound: change log types --- nonebot/__init__.py | 12 +++++---- nonebot/drivers/__init__.py | 3 ++- nonebot/drivers/fastapi.py | 19 +++++++------- nonebot/log.py | 51 ++++++++++++++++++++++++++++++------- nonebot/plugin.py | 12 ++++----- 5 files changed, 67 insertions(+), 30 deletions(-) diff --git a/nonebot/__init__.py b/nonebot/__init__.py index f5578073..db18d177 100644 --- a/nonebot/__init__.py +++ b/nonebot/__init__.py @@ -109,8 +109,8 @@ def get_bots() -> Dict[str, Bot]: return driver.bots -from nonebot.log import logger from nonebot.config import Env, Config +from nonebot.log import logger, default_filter from nonebot.adapters.cqhttp import Bot as CQBot try: @@ -147,11 +147,13 @@ def init(*, _env_file: Optional[str] = None, **kwargs): """ global _driver env = Env() - logger.debug(f"Current Env: {env.environment}") + logger.opt( + colors=True).debug(f"Current Env: {env.environment}") config = Config(**kwargs, _env_file=_env_file or f".env.{env.environment}") - logger.setLevel(logging.DEBUG if config.debug else logging.INFO) - logger.debug(f"Loaded config: {config.dict()}") + default_filter.level = "DEBUG" if config.debug else "INFO" + logger.opt( + colors=True).debug(f"Loaded Config: {config.dict()}") DriverClass: Type[Driver] = getattr(importlib.import_module(config.driver), "Driver") @@ -196,4 +198,4 @@ def run(host: Optional[str] = None, get_driver().run(host, port, *args, **kwargs) -from nonebot.plugin import load_plugins, get_loaded_plugins +from nonebot.plugin import load_plugin, load_plugins, get_loaded_plugins diff --git a/nonebot/drivers/__init__.py b/nonebot/drivers/__init__.py index 2a76f7cd..af8587d1 100644 --- a/nonebot/drivers/__init__.py +++ b/nonebot/drivers/__init__.py @@ -20,7 +20,8 @@ class BaseDriver(abc.ABC): @classmethod def register_adapter(cls, name: str, adapter: Type[Bot]): cls._adapters[name] = adapter - logger.debug(f'Succeeded to load adapter "{name}"') + logger.opt( + colors=True).debug(f'Succeeded to load adapter "{name}"') @property @abc.abstractmethod diff --git a/nonebot/drivers/fastapi.py b/nonebot/drivers/fastapi.py index 7dcb0527..3292f5df 100644 --- a/nonebot/drivers/fastapi.py +++ b/nonebot/drivers/fastapi.py @@ -84,17 +84,18 @@ class Driver(BaseDriver): LOGGING_CONFIG = { "version": 1, "disable_existing_loggers": False, - "formatters": { - "default": { - "()": "logging.Formatter", - "fmt": "[%(asctime)s %(name)s] %(levelname)s: %(message)s", - }, - }, + # "formatters": { + # "default": { + # "()": "logging.Formatter", + # "fmt": "[%(asctime)s %(name)s] %(levelname)s: %(message)s", + # }, + # }, "handlers": { "default": { - "formatter": "default", - "class": "logging.StreamHandler", - "stream": "ext://sys.stdout", + "class": "nonebot.log.LoguruHandler", + # "formatter": "default", + # "class": "logging.StreamHandler", + # "stream": "ext://sys.stdout", }, }, "loggers": { diff --git a/nonebot/log.py b/nonebot/log.py index 8218ca57..3881dbf4 100644 --- a/nonebot/log.py +++ b/nonebot/log.py @@ -13,7 +13,7 @@ NoneBot 使用 `loguru`_ 来记录日志信息。 """ import sys -# import logging +import logging from loguru import logger as logger_ @@ -35,10 +35,6 @@ logger = logger_ .. code-block:: python from nonebot.log import logger - - # 也可以这样 - import logging - logger = logging.getLogger("nonebot") """ # default_handler = logging.StreamHandler(sys.stdout) @@ -46,8 +42,45 @@ logger = logger_ # logging.Formatter("[%(asctime)s %(name)s] %(levelname)s: %(message)s")) # logger.addHandler(default_handler) + +class Filter: + + def __init__(self) -> None: + self.level = "DEBUG" + + def __call__(self, record): + record["name"] = record["name"].split(".")[0] + levelno = logger.level(self.level).no + return record["level"].no >= levelno + + +class LoguruHandler(logging.Handler): + + def emit(self, record): + try: + level = logger.level(record.levelname).name + except ValueError: + level = record.levelno + + frame, depth = logging.currentframe(), 2 + while frame.f_code.co_filename == logging.__file__: + frame = frame.f_back + depth += 1 + + logger.opt(depth=depth, + exception=record.exc_info).log(level, record.getMessage()) + + logger.remove() -default_format = ("{level: <8}|" - "{time:MM-DD HH:mm:ss}|" - "{message}") -logger.add(sys.stdout, colorize=True, format=default_format) +default_filter = Filter() +default_format = ( + "{time:MM-DD HH:mm:ss}|" + "{level: ^8}|" + "{name}| " + # "{function}:{line}| " + "{message}") +logger.add(sys.stdout, + colorize=True, + diagnose=False, + filter=default_filter, + format=default_format) diff --git a/nonebot/plugin.py b/nonebot/plugin.py index 0ea04046..51e38b11 100644 --- a/nonebot/plugin.py +++ b/nonebot/plugin.py @@ -170,11 +170,11 @@ def load_plugin(module_path: str) -> Optional[Plugin]: 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}\"") + logger.opt(colors=True).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) + logger.opt(colors=True, exception=e).error( + f'Failed to import "{module_path}"') return None @@ -196,10 +196,10 @@ def load_plugins(*plugin_dir: str) -> Set[Plugin]: plugin = Plugin(name, module, _tmp_matchers.copy()) plugins[name] = plugin loaded_plugins.add(plugin) - logger.info(f"Succeeded to import \"{name}\"") + logger.opt(colors=True).info(f'Succeeded to import "{name}"') except Exception as e: - logger.error(f"Failed to import \"{name}\", error: {e}") - logger.exception(e) + logger.opt(colors=True, exception=e).error( + f'Failed to import "{name}"') return loaded_plugins From 43bd9d0a961742ca2e26562d27ca233c232952d5 Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Thu, 27 Aug 2020 16:43:58 +0800 Subject: [PATCH 3/6] :loud_sound: add more logs --- nonebot/__init__.py | 39 ++++++++++++++++++++------------------ nonebot/adapters/cqhttp.py | 15 +++++++++++---- nonebot/drivers/fastapi.py | 12 +++--------- nonebot/log.py | 6 +++--- nonebot/matcher.py | 22 ++++++++++++++++++--- nonebot/message.py | 33 ++++++++++++++++++++------------ nonebot/plugin.py | 4 ++++ 7 files changed, 82 insertions(+), 49 deletions(-) diff --git a/nonebot/__init__.py b/nonebot/__init__.py index db18d177..7d49a9aa 100644 --- a/nonebot/__init__.py +++ b/nonebot/__init__.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import logging import importlib from nonebot.typing import Bot, Dict, Type, Union, Driver, Optional, NoReturn @@ -84,7 +83,7 @@ def get_asgi(): return driver.asgi -def get_bots() -> Dict[str, Bot]: +def get_bots() -> Union[NoReturn, Dict[str, Bot]]: """ :说明: @@ -146,26 +145,29 @@ def init(*, _env_file: Optional[str] = None, **kwargs): """ global _driver - env = Env() - logger.opt( - colors=True).debug(f"Current Env: {env.environment}") - config = Config(**kwargs, _env_file=_env_file or f".env.{env.environment}") + if not _driver: + logger.debug("NoneBot is initializing...") + env = Env() + logger.opt( + colors=True).debug(f"Current Env: {env.environment}") + config = Config(**kwargs, + _env_file=_env_file or f".env.{env.environment}") - default_filter.level = "DEBUG" if config.debug else "INFO" - logger.opt( - colors=True).debug(f"Loaded Config: {config.dict()}") + default_filter.level = "DEBUG" if config.debug else "INFO" + logger.opt( + colors=True).debug(f"Loaded Config: {config.dict()}") - DriverClass: Type[Driver] = getattr(importlib.import_module(config.driver), - "Driver") - _driver = DriverClass(env, config) + DriverClass: Type[Driver] = getattr( + importlib.import_module(config.driver), "Driver") + _driver = DriverClass(env, config) - # register build-in adapters - _driver.register_adapter("cqhttp", CQBot) + # register build-in adapters + _driver.register_adapter("cqhttp", CQBot) - # load nonebot test frontend if debug - if config.debug and nonebot_test: - logger.debug("Loading nonebot test frontend...") - nonebot_test.init() + # load nonebot test frontend if debug + if config.debug and nonebot_test: + logger.debug("Loading nonebot test frontend...") + nonebot_test.init() def run(host: Optional[str] = None, @@ -195,6 +197,7 @@ def run(host: Optional[str] = None, nonebot.run(host="127.0.0.1", port=8080) """ + logger.info("Running NoneBot...") get_driver().run(host, port, *args, **kwargs) diff --git a/nonebot/adapters/cqhttp.py b/nonebot/adapters/cqhttp.py index 3d071848..57742bc3 100644 --- a/nonebot/adapters/cqhttp.py +++ b/nonebot/adapters/cqhttp.py @@ -27,6 +27,10 @@ from nonebot.typing import overrides, Driver, WebSocket, NoReturn from nonebot.adapters import BaseBot, BaseEvent, BaseMessage, BaseMessageSegment +def log(level: str, message: str): + return logger.opt(colors=True).log(level, "CQHTTP | " + message) + + def escape(s: str, *, escape_comma: bool = True) -> str: """ 对字符串进行 CQ 码转义。 @@ -109,7 +113,7 @@ def _check_nickname(bot: "Bot", event: "Event"): re.IGNORECASE) if m: nickname = m.group(1) - logger.debug(f"User is calling me {nickname}") + log("DEBUG", f"User is calling me {nickname}") event.to_me = True first_msg_seg.data["text"] = first_text[m.end():] @@ -200,6 +204,7 @@ class Bot(BaseBot): bot = self.driver.bots[str(self_id)] return await bot.call_api(api, **data) + log("DEBUG", f"Calling API {api}") if self.type == "websocket": seq = ResultStore.get_seq() await self.websocket.send({ @@ -403,7 +408,7 @@ class MessageSegment(BaseMessageSegment): @overrides(BaseMessageSegment) def __init__(self, type: str, data: Dict[str, Union[str, list]]) -> None: if type == "text": - data["text"] = unescape(data["text"]) + data["text"] = unescape(data["text"]) # type: ignore super().__init__(type=type, data=data) @overrides(BaseMessageSegment) @@ -413,7 +418,9 @@ class MessageSegment(BaseMessageSegment): # process special types if type_ == "text": - return escape(data.get("text", ""), escape_comma=False) + return escape( + data.get("text", ""), # type: ignore + escape_comma=False) params = ",".join( [f"{k}={escape(str(v))}" for k, v in data.items() if v is not None]) @@ -449,7 +456,7 @@ class MessageSegment(BaseMessageSegment): @staticmethod def forward(id_: str) -> "MessageSegment": - logger.warning("Forward Message only can be received!") + log("WARNING", "Forward Message only can be received!") return MessageSegment("forward", {"id": id_}) @staticmethod diff --git a/nonebot/drivers/fastapi.py b/nonebot/drivers/fastapi.py index 3292f5df..21f56016 100644 --- a/nonebot/drivers/fastapi.py +++ b/nonebot/drivers/fastapi.py @@ -84,18 +84,9 @@ class Driver(BaseDriver): LOGGING_CONFIG = { "version": 1, "disable_existing_loggers": False, - # "formatters": { - # "default": { - # "()": "logging.Formatter", - # "fmt": "[%(asctime)s %(name)s] %(levelname)s: %(message)s", - # }, - # }, "handlers": { "default": { "class": "nonebot.log.LoguruHandler", - # "formatter": "default", - # "class": "logging.StreamHandler", - # "stream": "ext://sys.stdout", }, }, "loggers": { @@ -201,6 +192,9 @@ class Driver(BaseDriver): await ws.accept() self._clients[x_self_id] = bot + logger.opt(colors=True).info( + f"WebSocket Connection from {adapter.upper()} " + f"Bot {x_self_id} Accepted!") try: while not ws.closed: diff --git a/nonebot/log.py b/nonebot/log.py index 3881dbf4..f088fc98 100644 --- a/nonebot/log.py +++ b/nonebot/log.py @@ -74,9 +74,9 @@ class LoguruHandler(logging.Handler): logger.remove() default_filter = Filter() default_format = ( - "{time:MM-DD HH:mm:ss}|" - "{level: ^8}|" - "{name}| " + "{time:MM-DD HH:mm:ss} " + "[{level}] " + "{name} | " # "{function}:{line}| " "{message}") logger.add(sys.stdout, diff --git a/nonebot/matcher.py b/nonebot/matcher.py index 6ef7d8f1..e150f484 100644 --- a/nonebot/matcher.py +++ b/nonebot/matcher.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +from nonebot.log import logger import typing import inspect from functools import wraps @@ -19,9 +20,21 @@ current_bot: ContextVar = ContextVar("current_bot") current_event: ContextVar = ContextVar("current_event") -class Matcher: +class MatcherMeta(type): + + def __repr__(self) -> str: + return (f"") # type: ignore + + def __str__(self) -> str: + return self.__repr__() + + +class Matcher(metaclass=MatcherMeta): """`Matcher`类 """ + module: Optional[str] = None type: str = "" rule: Rule = Rule() @@ -43,8 +56,8 @@ class Matcher: self.state = self._default_state.copy() def __repr__(self) -> str: - return (f"") + return (f"") def __str__(self) -> str: return self.__repr__() @@ -59,6 +72,7 @@ class Matcher: priority: int = 1, block: bool = False, *, + module: Optional[str] = None, default_state: Optional[dict] = None, expire_time: Optional[datetime] = None) -> Type["Matcher"]: """创建新的 Matcher @@ -69,6 +83,7 @@ class Matcher: NewMatcher = type( "Matcher", (Matcher,), { + "module": module, "type": type_, "rule": rule, "permission": permission, @@ -253,5 +268,6 @@ class Matcher: except FinishedException: pass finally: + logger.info(f"Matcher {self} running complete") current_bot.reset(b_t) current_event.reset(e_t) diff --git a/nonebot/message.py b/nonebot/message.py index e6488e20..7caffbda 100644 --- a/nonebot/message.py +++ b/nonebot/message.py @@ -30,8 +30,8 @@ async def _run_matcher(Matcher: Type[Matcher], bot: Bot, event: Event, bot, event) or not await Matcher.check_rule(bot, event, state): return except Exception as e: - logger.error(f"Rule check failed for matcher {Matcher}. Ignored.") - logger.exception(e) + logger.opt(colors=True, exception=e).error( + f"Rule check failed for {Matcher}.") return # TODO: log matcher @@ -43,8 +43,9 @@ async def _run_matcher(Matcher: Type[Matcher], bot: Bot, event: Event, logger.debug(f"Running matcher {matcher}") await matcher.run(bot, event, state) except Exception as e: - logger.error(f"Running matcher {matcher} failed.") - logger.exception(e) + logger.opt(colors=True, exception=e).error( + f"Running matcher {matcher} failed." + ) exceptions = [] if Matcher.temp: @@ -56,20 +57,23 @@ async def _run_matcher(Matcher: Type[Matcher], bot: Bot, event: Event, async def handle_event(bot: Bot, event: Event): - log_msg = f"{bot.type.upper()} Bot {event.self_id} [{event.name}]: " + log_msg = f"{bot.type.upper()} | {event.self_id} [{event.name}]: " if event.type == "message": log_msg += f"Message {event.id} from " log_msg += str(event.user_id) if event.detail_type == "group": - log_msg += f"@[群:{event.group_id}]: " - log_msg += repr(str(event.message)) + log_msg += f"@[群:{event.group_id}]:" + + log_msg += ' "' + "".join( + map(lambda x: str(x) if x.type == "text" else f"{x!s}", + event.message)) + '"' # type: ignore elif event.type == "notice": log_msg += f"Notice {event.raw_event}" elif event.type == "request": log_msg += f"Request {event.raw_event}" elif event.type == "meta_event": log_msg += f"MetaEvent {event.raw_event}" - logger.info(log_msg) + logger.opt(colors=True).info(log_msg) coros = [] state = {} @@ -80,7 +84,8 @@ async def handle_event(bot: Bot, event: Event): logger.debug("Running PreProcessors...") await asyncio.gather(*coros) except IgnoredException: - logger.info(f"Event {event.name} is ignored") + logger.opt( + colors=True).info(f"Event {event.name} is ignored") return # Trie Match @@ -96,7 +101,7 @@ async def handle_event(bot: Bot, event: Event): for matcher in matchers[priority] ] - logger.debug(f"Checking for all matchers in priority {priority}...") + logger.debug(f"Checking for matchers in priority {priority}...") results = await asyncio.gather(*pending_tasks, return_exceptions=True) i = 0 @@ -104,8 +109,12 @@ async def handle_event(bot: Bot, event: Event): if isinstance(result, _ExceptionContainer): e_list = result.exceptions if StopPropagation in e_list: - break_flag = True - logger.debug("Stop event propagation") + if not break_flag: + break_flag = True + logger.debug("Stop event propagation") if ExpiredException in e_list: + logger.debug( + f"Matcher {matchers[priority][index - i]} will be removed." + ) del matchers[priority][index - i] i += 1 diff --git a/nonebot/plugin.py b/nonebot/plugin.py index 51e38b11..b71f1f61 100644 --- a/nonebot/plugin.py +++ b/nonebot/plugin.py @@ -168,6 +168,8 @@ def load_plugin(module_path: str) -> Optional[Plugin]: try: _tmp_matchers.clear() module = importlib.import_module(module_path) + for m in _tmp_matchers: + m.module = module_path plugin = Plugin(module_path, module, _tmp_matchers.copy()) plugins[module_path] = plugin logger.opt(colors=True).info(f'Succeeded to import "{module_path}"') @@ -193,6 +195,8 @@ def load_plugins(*plugin_dir: str) -> Set[Plugin]: try: module = _load(spec) + for m in _tmp_matchers: + m.module = name plugin = Plugin(name, module, _tmp_matchers.copy()) plugins[name] = plugin loaded_plugins.add(plugin) From def5caedbcb00ce6088cfb3a409fac44cc0a29ea Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Fri, 28 Aug 2020 11:54:21 +0800 Subject: [PATCH 4/6] :sparkles: check for reply --- nonebot/adapters/__init__.py | 10 ++++++++++ nonebot/adapters/cqhttp.py | 32 +++++++++++++++++++++++++++----- nonebot/config.py | 4 ++-- nonebot/matcher.py | 2 +- nonebot/message.py | 1 - 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/nonebot/adapters/__init__.py b/nonebot/adapters/__init__.py index efc34acc..a4d10c2d 100644 --- a/nonebot/adapters/__init__.py +++ b/nonebot/adapters/__init__.py @@ -150,6 +150,16 @@ class BaseEvent(abc.ABC): def message(self, value) -> None: raise NotImplementedError + @property + @abc.abstractmethod + def reply(self) -> Optional[dict]: + raise NotImplementedError + + @reply.setter + @abc.abstractmethod + def reply(self, value) -> None: + raise NotImplementedError + @property @abc.abstractmethod def raw_message(self) -> Optional[str]: diff --git a/nonebot/adapters/cqhttp.py b/nonebot/adapters/cqhttp.py index 57742bc3..6424c1d0 100644 --- a/nonebot/adapters/cqhttp.py +++ b/nonebot/adapters/cqhttp.py @@ -57,6 +57,19 @@ def _b2s(b: Optional[bool]) -> Optional[str]: return b if b is None else str(b).lower() +async def _check_reply(bot: "Bot", event: "Event"): + if event.type != "message": + return + + first_msg_seg = event.message[0] + if first_msg_seg.type == "reply": + msg_id = first_msg_seg.data["id"] + event.reply = await bot.get_msg(message_id=msg_id) + if event.reply["sender"]["user_id"] == event.self_id: + event.to_me = True + del event.message[0] + + def _check_at_me(bot: "Bot", event: "Event"): if event.type != "message": return @@ -64,7 +77,6 @@ def _check_at_me(bot: "Bot", event: "Event"): if event.detail_type == "private": event.to_me = True else: - event.to_me = False at_me_seg = MessageSegment.at(event.self_id) # check the first segment @@ -150,7 +162,7 @@ class ResultStore: try: return await asyncio.wait_for(future, timeout) except asyncio.TimeoutError: - raise NetworkError("WebSocket API call timeout") + raise NetworkError("WebSocket API call timeout") from None finally: del cls._futures[seq] @@ -190,7 +202,7 @@ class Bot(BaseBot): event = Event(message) # Check whether user is calling me - # TODO: Check reply + await _check_reply(self, event) _check_at_me(self, event) _check_nickname(self, event) @@ -205,7 +217,7 @@ class Bot(BaseBot): return await bot.call_api(api, **data) log("DEBUG", f"Calling API {api}") - if self.type == "websocket": + if self.connection_type == "websocket": seq = ResultStore.get_seq() await self.websocket.send({ "action": api, @@ -217,7 +229,7 @@ class Bot(BaseBot): return _handle_api_result(await ResultStore.fetch( seq, self.config.api_timeout)) - elif self.type == "http": + elif self.connection_type == "http": api_root = self.config.api_root.get(self.self_id) if not api_root: raise ApiNotAvailable @@ -377,6 +389,16 @@ class Event(BaseEvent): def message(self, value) -> None: self._raw_event["message"] = value + @property + @overrides(BaseEvent) + def reply(self) -> Optional[dict]: + return self._raw_event.get("reply") + + @reply.setter + @overrides(BaseEvent) + def reply(self, value) -> None: + self._raw_event["reply"] = value + @property @overrides(BaseEvent) def raw_message(self) -> Optional[str]: diff --git a/nonebot/config.py b/nonebot/config.py index c80cc32e..5ecc93e6 100644 --- a/nonebot/config.py +++ b/nonebot/config.py @@ -174,10 +174,10 @@ class Config(BaseConfig): API_ROOT={"123456": "http://127.0.0.1:5700"} """ - api_timeout: Optional[float] = 60. + api_timeout: Optional[float] = 30. """ - 类型: ``Optional[float]`` - - 默认值: ``60.`` + - 默认值: ``30.`` - 说明: API 请求超时时间,单位: 秒。 """ diff --git a/nonebot/matcher.py b/nonebot/matcher.py index e150f484..351661b9 100644 --- a/nonebot/matcher.py +++ b/nonebot/matcher.py @@ -28,7 +28,7 @@ class MatcherMeta(type): f"temp={self.temp}>") # type: ignore def __str__(self) -> str: - return self.__repr__() + return repr(self) class Matcher(metaclass=MatcherMeta): diff --git a/nonebot/message.py b/nonebot/message.py index 7caffbda..b6c3d888 100644 --- a/nonebot/message.py +++ b/nonebot/message.py @@ -34,7 +34,6 @@ async def _run_matcher(Matcher: Type[Matcher], bot: Bot, event: Event, f"Rule check failed for {Matcher}.") return - # TODO: log matcher logger.info(f"Event will be handled by {Matcher}") matcher = Matcher() From f5b655ef71d8bb0622a9a396cce1de53a8c26dfa Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Sat, 29 Aug 2020 21:59:36 +0800 Subject: [PATCH 5/6] :wrench: change command and add builtin plugin --- nonebot/__init__.py | 4 +- nonebot/plugin.py | 54 ++++++++++++++----- nonebot/plugins/base.py | 10 ++++ nonebot/rule.py | 18 +++++-- tests/bot.py | 12 +++++ .../test_plugins/test_package/test_command.py | 2 +- 6 files changed, 79 insertions(+), 21 deletions(-) create mode 100644 nonebot/plugins/base.py diff --git a/nonebot/__init__.py b/nonebot/__init__.py index 7d49a9aa..5e7a2034 100644 --- a/nonebot/__init__.py +++ b/nonebot/__init__.py @@ -201,4 +201,6 @@ def run(host: Optional[str] = None, get_driver().run(host, port, *args, **kwargs) -from nonebot.plugin import load_plugin, load_plugins, get_loaded_plugins +from nonebot.plugin import on_message, on_notice, on_request, on_metaevent +from nonebot.plugin import on_startswith, on_endswith, on_command, on_regex +from nonebot.plugin import load_plugin, load_plugins, load_builtin_plugins, get_loaded_plugins diff --git a/nonebot/plugin.py b/nonebot/plugin.py index b71f1f61..6241687d 100644 --- a/nonebot/plugin.py +++ b/nonebot/plugin.py @@ -10,8 +10,9 @@ from importlib._bootstrap import _load from nonebot.log import logger from nonebot.matcher import Matcher from nonebot.permission import Permission +from nonebot.typing import Handler, RuleChecker from nonebot.rule import Rule, startswith, endswith, command, regex -from nonebot.typing import Set, Dict, Type, Tuple, Union, Optional, ModuleType, RuleChecker +from nonebot.typing import Set, List, Dict, Type, Tuple, Union, Optional, ModuleType plugins: Dict[str, "Plugin"] = {} @@ -20,10 +21,9 @@ _tmp_matchers: Set[Type[Matcher]] = set() class Plugin(object): - # TODO: store plugin informations - def __init__(self, module_path: str, module: ModuleType, + def __init__(self, name: str, module: ModuleType, matchers: Set[Type[Matcher]]): - self.module_path = module_path + self.name = name self.module = module self.matchers = matchers @@ -31,7 +31,7 @@ class Plugin(object): def on(rule: Union[Rule, RuleChecker] = Rule(), permission: Permission = Permission(), *, - handlers: Optional[list] = None, + handlers: Optional[List[Handler]] = None, temp: bool = False, priority: int = 1, block: bool = False, @@ -50,7 +50,7 @@ def on(rule: Union[Rule, RuleChecker] = Rule(), def on_metaevent(rule: Union[Rule, RuleChecker] = Rule(), *, - handlers: Optional[list] = None, + handlers: Optional[List[Handler]] = None, temp: bool = False, priority: int = 1, block: bool = False, @@ -70,7 +70,7 @@ def on_metaevent(rule: Union[Rule, RuleChecker] = Rule(), def on_message(rule: Union[Rule, RuleChecker] = Rule(), permission: Permission = Permission(), *, - handlers: Optional[list] = None, + handlers: Optional[List[Handler]] = None, temp: bool = False, priority: int = 1, block: bool = True, @@ -89,7 +89,7 @@ def on_message(rule: Union[Rule, RuleChecker] = Rule(), def on_notice(rule: Union[Rule, RuleChecker] = Rule(), *, - handlers: Optional[list] = None, + handlers: Optional[List[Handler]] = None, temp: bool = False, priority: int = 1, block: bool = False, @@ -108,7 +108,7 @@ def on_notice(rule: Union[Rule, RuleChecker] = Rule(), def on_request(rule: Union[Rule, RuleChecker] = Rule(), *, - handlers: Optional[list] = None, + handlers: Optional[List[Handler]] = None, temp: bool = False, priority: int = 1, block: bool = False, @@ -149,9 +149,19 @@ def on_command(cmd: Union[str, Tuple[str, ...]], **kwargs) -> Type[Matcher]: if isinstance(cmd, str): cmd = (cmd,) - return on_message(command(cmd) & - rule, permission, **kwargs) if rule else on_message( - command(cmd), permission, **kwargs) + + async def _strip_cmd(bot, event, state: dict): + message = event.message + event.message = message.__class__( + str(message)[len(state["_prefix"]["raw_command"]):].strip()) + + handlers = kwargs.pop("handlers", []) + handlers.insert(0, _strip_cmd) + + return on_message( + command(cmd) & + rule, permission, handlers=handlers, **kwargs) if rule else on_message( + command(cmd), permission, handlers=handlers, **kwargs) def on_regex(pattern: str, @@ -167,12 +177,20 @@ def on_regex(pattern: str, def load_plugin(module_path: str) -> Optional[Plugin]: try: _tmp_matchers.clear() + if module_path in plugins: + return plugins[module_path] + elif module_path in sys.modules: + logger.warning( + f"Module {module_path} has been loaded by other plugins! Ignored" + ) + return module = importlib.import_module(module_path) for m in _tmp_matchers: m.module = module_path plugin = Plugin(module_path, module, _tmp_matchers.copy()) plugins[module_path] = plugin - logger.opt(colors=True).info(f'Succeeded to import "{module_path}"') + logger.opt( + colors=True).info(f'Succeeded to import "{module_path}"') return plugin except Exception as e: logger.opt(colors=True, exception=e).error( @@ -189,7 +207,11 @@ def load_plugins(*plugin_dir: str) -> Set[Plugin]: continue spec = module_info.module_finder.find_spec(name) - if spec.name in sys.modules: + if spec.name in plugins: + continue + elif spec.name in sys.modules: + logger.warning( + f"Module {spec.name} has been loaded by other plugin! Ignored") continue try: @@ -207,5 +229,9 @@ def load_plugins(*plugin_dir: str) -> Set[Plugin]: return loaded_plugins +def load_builtin_plugins(): + return load_plugin("nonebot.plugins.base") + + def get_loaded_plugins() -> Set[Plugin]: return set(plugins.values()) diff --git a/nonebot/plugins/base.py b/nonebot/plugins/base.py new file mode 100644 index 00000000..de743d14 --- /dev/null +++ b/nonebot/plugins/base.py @@ -0,0 +1,10 @@ +from nonebot.rule import to_me +from nonebot.plugin import on_command +from nonebot.typing import Bot, Event + +say = on_command("say", to_me()) + + +@say.handle() +async def repeat(bot: Bot, event: Event, state: dict): + await bot.send(message=event.message, event=event) diff --git a/nonebot/rule.py b/nonebot/rule.py index 34c1c7b4..a992e488 100644 --- a/nonebot/rule.py +++ b/nonebot/rule.py @@ -74,13 +74,21 @@ class TrieRule: suffix = cls.suffix.longest_prefix( message_r.data["text"].rstrip()[::-1]) - state["_prefix"] = {prefix.key: prefix.value} if prefix else {} - state["_suffix"] = {suffix.key: suffix.value} if suffix else {} + state["_prefix"] = { + "raw_command": prefix.key, + "command": prefix.value + } if prefix else {} + state["_suffix"] = { + "raw_command": suffix.key, + "command": suffix.value + } if suffix else {} return ({ - prefix.key: prefix.value + "raw_command": prefix.key, + "command": prefix.value } if prefix else {}, { - suffix.key: suffix.value + "raw_command": suffix.key, + "command": suffix.value } if suffix else {}) @@ -122,7 +130,7 @@ def command(command: Tuple[str, ...]) -> Rule: TrieRule.add_prefix(f"{start}{sep.join(command)}", command) async def _command(bot: Bot, event: Event, state: dict) -> bool: - return command in state["_prefix"].values() + return command == state["_prefix"]["command"] return Rule(_command) diff --git a/tests/bot.py b/tests/bot.py index 095e2eab..31ffb6c8 100644 --- a/tests/bot.py +++ b/tests/bot.py @@ -7,10 +7,22 @@ import sys sys.path.insert(0, os.path.abspath("..")) import nonebot +from nonebot.log import logger, default_format + +# test custom log +logger.add("error.log", + rotation="00:00", + diagnose=False, + level="ERROR", + format=default_format) nonebot.init() app = nonebot.get_asgi() +# load builtin plugin +nonebot.load_plugin("nonebot.plugins.base") + +# load local plugins nonebot.load_plugins("test_plugins") if __name__ == "__main__": diff --git a/tests/test_plugins/test_package/test_command.py b/tests/test_plugins/test_package/test_command.py index 8c811ddc..af060fff 100644 --- a/tests/test_plugins/test_package/test_command.py +++ b/tests/test_plugins/test_package/test_command.py @@ -11,7 +11,7 @@ test_command = on_command("帮助", to_me()) @test_command.handle() async def test_handler(bot: Bot, event: Event, state: dict): - args = str(event.message)[len(list(state["_prefix"].keys())[0]):].strip() + args = str(event.message).strip() print("[!] Command:", state["_prefix"], "Args:", args) if args: state["help"] = args From e0e23283c15bc7fa58132b7d7bbcf6c75458ee20 Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Sat, 29 Aug 2020 22:32:40 +0800 Subject: [PATCH 6/6] :building_construction: store plugin info as dataclass --- nonebot/plugin.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/nonebot/plugin.py b/nonebot/plugin.py index 6241687d..ba526fcf 100644 --- a/nonebot/plugin.py +++ b/nonebot/plugin.py @@ -5,6 +5,7 @@ import re import sys import pkgutil import importlib +from dataclasses import dataclass from importlib._bootstrap import _load from nonebot.log import logger @@ -19,13 +20,11 @@ plugins: Dict[str, "Plugin"] = {} _tmp_matchers: Set[Type[Matcher]] = set() +@dataclass(eq=False) class Plugin(object): - - def __init__(self, name: str, module: ModuleType, - matchers: Set[Type[Matcher]]): - self.name = name - self.module = module - self.matchers = matchers + name: str + module: ModuleType + matcher: Set[Type[Matcher]] def on(rule: Union[Rule, RuleChecker] = Rule(),