From f0bc47ec5e2cbe28b971ace3b59585fe9d34e8a8 Mon Sep 17 00:00:00 2001 From: Mix <32300164+mnixry@users.noreply.github.com> Date: Fri, 27 Aug 2021 02:52:24 +0800 Subject: [PATCH 1/6] :sparkles: Add message template formatter ref: https://github.com/nonebot/discussions/discussions/27 --- nonebot/adapters/_base.py | 20 ++++-- nonebot/adapters/_formatter.py | 118 +++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 nonebot/adapters/_formatter.py diff --git a/nonebot/adapters/_base.py b/nonebot/adapters/_base.py index 10932667..123914e6 100644 --- a/nonebot/adapters/_base.py +++ b/nonebot/adapters/_base.py @@ -8,19 +8,21 @@ import abc import asyncio from copy import deepcopy +from dataclasses import asdict, dataclass, field from functools import partial -from typing_extensions import Protocol -from dataclasses import dataclass, field, asdict -from typing import (Any, Set, List, Dict, Type, Tuple, Union, TypeVar, Mapping, - Generic, Optional, Iterable) +from typing import (Any, Dict, Generic, Iterable, List, Mapping, Optional, Set, + Tuple, Type, TypeVar, Union) from pydantic import BaseModel +from typing_extensions import Protocol -from nonebot.log import logger from nonebot.config import Config -from nonebot.utils import DataclassEncoder from nonebot.drivers import Driver, HTTPConnection, HTTPResponse -from nonebot.typing import T_CallingAPIHook, T_CalledAPIHook +from nonebot.log import logger +from nonebot.typing import T_CalledAPIHook, T_CallingAPIHook +from nonebot.utils import DataclassEncoder + +from ._formatter import MessageFormatter class _ApiCall(Protocol): @@ -329,6 +331,10 @@ class Message(List[TMS], abc.ABC): else: self.extend(self._construct(message)) + @classmethod + def template(cls: Type[TM], format_string: str) -> MessageFormatter[TM]: + return MessageFormatter(cls, format_string) + @classmethod @abc.abstractmethod def get_segment_class(cls) -> Type[TMS]: diff --git a/nonebot/adapters/_formatter.py b/nonebot/adapters/_formatter.py new file mode 100644 index 00000000..1c11afa3 --- /dev/null +++ b/nonebot/adapters/_formatter.py @@ -0,0 +1,118 @@ +import functools +import operator +from string import Formatter +from typing import (Any, Generic, List, Mapping, Protocol, Sequence, Set, Tuple, + Type, TypeVar, Union, TYPE_CHECKING) + +if TYPE_CHECKING: + from nonebot.adapters import Message + + +class AddAble(Protocol): + + def __add__(self, __s: Any) -> "AddAble": + ... + + def __str__(self) -> str: + ... + + +AddAble_T = TypeVar("AddAble_T", bound=AddAble) +MessageResult_T = TypeVar("MessageResult_T", bound="Message", covariant=True) + + +class MessageFormatter(Formatter, Generic[MessageResult_T]): + + def __init__(self, factory: Type[MessageResult_T], template: str) -> None: + super().__init__() + self.template = template + self.factory = factory + + def format(self, *args: AddAble, **kwargs: AddAble) -> MessageResult_T: + msg: AddAble = super().format(self.template, *args, **kwargs) + return msg if isinstance(msg, self.factory) else self.factory( + msg) # type: ignore + + def vformat(self, format_string: str, args: Sequence[AddAble], + kwargs: Mapping[str, AddAble]): + result, arg_index, used_args = self._vformat(format_string, args, + kwargs, set(), 2) + self.check_unused_args(list(used_args), args, kwargs) + return result + + def _vformat( + self, + format_string: str, + args: Sequence[Any], + kwargs: Mapping[str, Any], + used_args: Set[Union[int, str]], + recursion_depth: int, + auto_arg_index: int = 0, + ) -> Tuple[AddAble, int, Set[Union[int, str]]]: + + if recursion_depth < 0: + raise ValueError("Max string recursion exceeded") + + results: List[AddAble] = [] + + for (literal_text, field_name, format_spec, + conversion) in self.parse(format_string): + + # output the literal text + if literal_text: + results.append(literal_text) + + # if there's a field, output it + if field_name is not None: + # this is some markup, find the object and do + # the formatting + + # handle arg indexing when empty field_names are given. + if field_name == "": + if auto_arg_index is False: + raise ValueError( + "cannot switch from manual field specification to " + "automatic field numbering") + field_name = str(auto_arg_index) + auto_arg_index += 1 + elif field_name.isdigit(): + if auto_arg_index: + raise ValueError( + "cannot switch from manual field specification to " + "automatic field numbering") + # disable auto arg incrementing, if it gets + # used later on, then an exception will be raised + auto_arg_index = False + + # given the field_name, find the object it references + # and the argument it came from + obj, arg_used = self.get_field(field_name, args, kwargs) + used_args.add(arg_used) + + assert format_spec is not None + + # do any conversion on the resulting object + obj = self.convert_field(obj, conversion) if conversion else obj + + # expand the format spec, if needed + format_control, auto_arg_index, formatted_args = self._vformat( + format_spec, + args, + kwargs, + used_args.copy(), + recursion_depth - 1, + auto_arg_index, + ) + used_args |= formatted_args + + # format the object and append to the result + formatted_text = self.format_field(obj, str(format_control)) + results.append(formatted_text) + + return functools.reduce(operator.add, results or + [""]), auto_arg_index, used_args + + def format_field(self, value: AddAble_T, + format_spec: str) -> Union[AddAble_T, str]: + return super().format_field(value, + format_spec) if format_spec else value From 58d10abd325f7cdeef087f9fee9aea7d390dd746 Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Fri, 27 Aug 2021 14:46:15 +0800 Subject: [PATCH 2/6] :art: change typing for formatter --- nonebot/adapters/_base.py | 18 +++++----- nonebot/adapters/_formatter.py | 65 ++++++++++++++-------------------- 2 files changed, 37 insertions(+), 46 deletions(-) diff --git a/nonebot/adapters/_base.py b/nonebot/adapters/_base.py index 123914e6..387cf316 100644 --- a/nonebot/adapters/_base.py +++ b/nonebot/adapters/_base.py @@ -8,19 +8,19 @@ import abc import asyncio from copy import deepcopy -from dataclasses import asdict, dataclass, field from functools import partial -from typing import (Any, Dict, Generic, Iterable, List, Mapping, Optional, Set, - Tuple, Type, TypeVar, Union) +from typing_extensions import Protocol +from dataclasses import asdict, dataclass, field +from typing import (Any, Set, List, Dict, Type, Tuple, Union, TypeVar, Mapping, + Generic, Optional, Iterable) from pydantic import BaseModel -from typing_extensions import Protocol -from nonebot.config import Config -from nonebot.drivers import Driver, HTTPConnection, HTTPResponse from nonebot.log import logger -from nonebot.typing import T_CalledAPIHook, T_CallingAPIHook +from nonebot.config import Config from nonebot.utils import DataclassEncoder +from nonebot.typing import T_CallingAPIHook, T_CalledAPIHook +from nonebot.drivers import Driver, HTTPConnection, HTTPResponse from ._formatter import MessageFormatter @@ -332,7 +332,9 @@ class Message(List[TMS], abc.ABC): self.extend(self._construct(message)) @classmethod - def template(cls: Type[TM], format_string: str) -> MessageFormatter[TM]: + def template( + cls: Type[TM], + format_string: str) -> MessageFormatter[TM, TMS]: # type: ignore return MessageFormatter(cls, format_string) @classmethod diff --git a/nonebot/adapters/_formatter.py b/nonebot/adapters/_formatter.py index 1c11afa3..9efc004c 100644 --- a/nonebot/adapters/_formatter.py +++ b/nonebot/adapters/_formatter.py @@ -1,59 +1,49 @@ import functools import operator from string import Formatter -from typing import (Any, Generic, List, Mapping, Protocol, Sequence, Set, Tuple, - Type, TypeVar, Union, TYPE_CHECKING) +from typing import (Any, Set, List, Type, Tuple, Union, TypeVar, Mapping, + Generic, Sequence, TYPE_CHECKING) if TYPE_CHECKING: - from nonebot.adapters import Message + from . import Message, MessageSegment + +TM = TypeVar("TM", bound="Message") +TMS = TypeVar("TMS", bound="MessageSegment") +TAddable = Union[str, TM, TMS] -class AddAble(Protocol): +class MessageFormatter(Formatter, Generic[TM, TMS]): - def __add__(self, __s: Any) -> "AddAble": - ... - - def __str__(self) -> str: - ... - - -AddAble_T = TypeVar("AddAble_T", bound=AddAble) -MessageResult_T = TypeVar("MessageResult_T", bound="Message", covariant=True) - - -class MessageFormatter(Formatter, Generic[MessageResult_T]): - - def __init__(self, factory: Type[MessageResult_T], template: str) -> None: - super().__init__() + def __init__(self, factory: Type[TM], template: str) -> None: self.template = template self.factory = factory - def format(self, *args: AddAble, **kwargs: AddAble) -> MessageResult_T: - msg: AddAble = super().format(self.template, *args, **kwargs) - return msg if isinstance(msg, self.factory) else self.factory( - msg) # type: ignore + def format(self, *args: TAddable[TM, TMS], **kwargs: TAddable[TM, + TMS]) -> TM: + msg = self.vformat(self.template, args, kwargs) + return msg if isinstance(msg, self.factory) else self.factory(msg) - def vformat(self, format_string: str, args: Sequence[AddAble], - kwargs: Mapping[str, AddAble]): - result, arg_index, used_args = self._vformat(format_string, args, - kwargs, set(), 2) + def vformat(self, format_string: str, args: Sequence[TAddable[TM, TMS]], + kwargs: Mapping[str, TAddable[TM, TMS]]) -> TM: + used_args = set() + result, _ = self._vformat(format_string, args, kwargs, used_args, 2) self.check_unused_args(list(used_args), args, kwargs) return result def _vformat( self, format_string: str, - args: Sequence[Any], - kwargs: Mapping[str, Any], + args: Sequence[TAddable[TM, TMS]], + kwargs: Mapping[str, TAddable[TM, TMS]], used_args: Set[Union[int, str]], recursion_depth: int, auto_arg_index: int = 0, - ) -> Tuple[AddAble, int, Set[Union[int, str]]]: + ) -> Tuple[TM, int]: if recursion_depth < 0: raise ValueError("Max string recursion exceeded") - results: List[AddAble] = [] + results: List[TAddable[TM, TMS]] = [] for (literal_text, field_name, format_spec, conversion) in self.parse(format_string): @@ -95,24 +85,23 @@ class MessageFormatter(Formatter, Generic[MessageResult_T]): obj = self.convert_field(obj, conversion) if conversion else obj # expand the format spec, if needed - format_control, auto_arg_index, formatted_args = self._vformat( + format_control, auto_arg_index = self._vformat( format_spec, args, kwargs, - used_args.copy(), + used_args, recursion_depth - 1, auto_arg_index, ) - used_args |= formatted_args # format the object and append to the result formatted_text = self.format_field(obj, str(format_control)) results.append(formatted_text) - return functools.reduce(operator.add, results or - [""]), auto_arg_index, used_args + return self.factory(functools.reduce(operator.add, results or + [""])), auto_arg_index - def format_field(self, value: AddAble_T, - format_spec: str) -> Union[AddAble_T, str]: + def format_field(self, value: TAddable[TM, TMS], + format_spec: str) -> TAddable[TM, TMS]: return super().format_field(value, format_spec) if format_spec else value From 7cfdc2dd37198103a912280a60275b84efb52e13 Mon Sep 17 00:00:00 2001 From: yanyongyu Date: Fri, 27 Aug 2021 15:08:26 +0800 Subject: [PATCH 3/6] :fire: use Any for format type --- nonebot/adapters/_base.py | 6 ++---- nonebot/adapters/_formatter.py | 24 ++++++++---------------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/nonebot/adapters/_base.py b/nonebot/adapters/_base.py index 387cf316..30fa312a 100644 --- a/nonebot/adapters/_base.py +++ b/nonebot/adapters/_base.py @@ -10,7 +10,7 @@ import asyncio from copy import deepcopy from functools import partial from typing_extensions import Protocol -from dataclasses import asdict, dataclass, field +from dataclasses import dataclass, field, asdict from typing import (Any, Set, List, Dict, Type, Tuple, Union, TypeVar, Mapping, Generic, Optional, Iterable) @@ -332,9 +332,7 @@ class Message(List[TMS], abc.ABC): self.extend(self._construct(message)) @classmethod - def template( - cls: Type[TM], - format_string: str) -> MessageFormatter[TM, TMS]: # type: ignore + def template(cls: Type[TM], format_string: str) -> MessageFormatter[TM]: return MessageFormatter(cls, format_string) @classmethod diff --git a/nonebot/adapters/_formatter.py b/nonebot/adapters/_formatter.py index 9efc004c..9dad44d1 100644 --- a/nonebot/adapters/_formatter.py +++ b/nonebot/adapters/_formatter.py @@ -5,26 +5,23 @@ from typing import (Any, Set, List, Type, Tuple, Union, TypeVar, Mapping, Generic, Sequence, TYPE_CHECKING) if TYPE_CHECKING: - from . import Message, MessageSegment + from . import Message TM = TypeVar("TM", bound="Message") -TMS = TypeVar("TMS", bound="MessageSegment") -TAddable = Union[str, TM, TMS] -class MessageFormatter(Formatter, Generic[TM, TMS]): +class MessageFormatter(Formatter, Generic[TM]): def __init__(self, factory: Type[TM], template: str) -> None: self.template = template self.factory = factory - def format(self, *args: TAddable[TM, TMS], **kwargs: TAddable[TM, - TMS]) -> TM: + def format(self, *args: Any, **kwargs: Any) -> TM: msg = self.vformat(self.template, args, kwargs) return msg if isinstance(msg, self.factory) else self.factory(msg) - def vformat(self, format_string: str, args: Sequence[TAddable[TM, TMS]], - kwargs: Mapping[str, TAddable[TM, TMS]]) -> TM: + def vformat(self, format_string: str, args: Sequence[Any], + kwargs: Mapping[str, Any]) -> TM: used_args = set() result, _ = self._vformat(format_string, args, kwargs, used_args, 2) self.check_unused_args(list(used_args), args, kwargs) @@ -33,8 +30,8 @@ class MessageFormatter(Formatter, Generic[TM, TMS]): def _vformat( self, format_string: str, - args: Sequence[TAddable[TM, TMS]], - kwargs: Mapping[str, TAddable[TM, TMS]], + args: Sequence[Any], + kwargs: Mapping[str, Any], used_args: Set[Union[int, str]], recursion_depth: int, auto_arg_index: int = 0, @@ -43,7 +40,7 @@ class MessageFormatter(Formatter, Generic[TM, TMS]): if recursion_depth < 0: raise ValueError("Max string recursion exceeded") - results: List[TAddable[TM, TMS]] = [] + results: List[Any] = [] for (literal_text, field_name, format_spec, conversion) in self.parse(format_string): @@ -100,8 +97,3 @@ class MessageFormatter(Formatter, Generic[TM, TMS]): return self.factory(functools.reduce(operator.add, results or [""])), auto_arg_index - - def format_field(self, value: TAddable[TM, TMS], - format_spec: str) -> TAddable[TM, TMS]: - return super().format_field(value, - format_spec) if format_spec else value From 1c73a9adfac143a1ae027ee0916768d5db557f14 Mon Sep 17 00:00:00 2001 From: Mix <32300164+mnixry@users.noreply.github.com> Date: Fri, 27 Aug 2021 19:03:46 +0800 Subject: [PATCH 4/6] :bulb: add comment to describe template formatter usage --- nonebot/adapters/_base.py | 22 ++++++++++++++++++++++ nonebot/adapters/_formatter.py | 6 ++++++ 2 files changed, 28 insertions(+) diff --git a/nonebot/adapters/_base.py b/nonebot/adapters/_base.py index 30fa312a..223dea6d 100644 --- a/nonebot/adapters/_base.py +++ b/nonebot/adapters/_base.py @@ -333,6 +333,28 @@ class Message(List[TMS], abc.ABC): @classmethod def template(cls: Type[TM], format_string: str) -> MessageFormatter[TM]: + """ + :说明: + + 根据创建消息模板, 用法和 ``str.format`` 大致相同, 但是可以输出消息对象 + + :示例: + + .. code-block:: python + + >>> Message.template("{} {}").format("hello", "world") + Message(MessageSegment(type='text', data={'text': 'hello world'})) + >>> Message.template("{} {}").format(MessageSegment.image("file///..."), "world") + Message(MessageSegment(type='image', data={'file': 'file///...'}), MessageSegment(type='text', data={'text': 'world'})) + + :参数: + + * ``format_string: str``: 格式化字符串 + + :返回: + + - ``MessageFormatter[TM]``: 消息格式化器 + """ return MessageFormatter(cls, format_string) @classmethod diff --git a/nonebot/adapters/_formatter.py b/nonebot/adapters/_formatter.py index 9dad44d1..68994f42 100644 --- a/nonebot/adapters/_formatter.py +++ b/nonebot/adapters/_formatter.py @@ -11,12 +11,18 @@ TM = TypeVar("TM", bound="Message") class MessageFormatter(Formatter, Generic[TM]): + """消息模板格式化实现类""" def __init__(self, factory: Type[TM], template: str) -> None: self.template = template self.factory = factory def format(self, *args: Any, **kwargs: Any) -> TM: + """ + :说明: + + 根据模板和参数生成消息对象 + """ msg = self.vformat(self.template, args, kwargs) return msg if isinstance(msg, self.factory) else self.factory(msg) From 01e818f6a4763e6d1fafd61f0ab4b6ec0415a951 Mon Sep 17 00:00:00 2001 From: Mix <32300164+mnixry@users.noreply.github.com> Date: Fri, 27 Aug 2021 19:24:12 +0800 Subject: [PATCH 5/6] :memo: allow document generate from `nonebot.adapters._formatter` --- docs_build/adapters/README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs_build/adapters/README.rst b/docs_build/adapters/README.rst index 8e759794..14f91a77 100644 --- a/docs_build/adapters/README.rst +++ b/docs_build/adapters/README.rst @@ -11,3 +11,9 @@ NoneBot.adapters 模块 :private-members: :special-members: __init__ :show-inheritance: + +.. automodule:: nonebot.adapters._formatter + :members: + :private-members: + :special-members: __init__ + :show-inheritance: \ No newline at end of file From ca26609308926d06f939ed6ae0b8a93de74cc87d Mon Sep 17 00:00:00 2001 From: nonebot Date: Fri, 27 Aug 2021 11:25:23 +0000 Subject: [PATCH 6/6] :memo: update api docs --- docs/api/adapters/README.md | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/docs/api/adapters/README.md b/docs/api/adapters/README.md index d1431b1f..2e8555c8 100644 --- a/docs/api/adapters/README.md +++ b/docs/api/adapters/README.md @@ -300,6 +300,40 @@ await bot.send_msg(message="hello world") +### _classmethod_ `template(format_string)` + + +* **说明** + + 根据创建消息模板, 用法和 `str.format` 大致相同, 但是可以输出消息对象 + + + +* **示例** + + +```python +>>> Message.template("{} {}").format("hello", "world") +Message(MessageSegment(type='text', data={'text': 'hello world'})) +>>> Message.template("{} {}").format(MessageSegment.image("file///..."), "world") +Message(MessageSegment(type='image', data={'file': 'file///...'}), MessageSegment(type='text', data={'text': 'world'})) +``` + + +* **参数** + + + * `format_string: str`: 格式化字符串 + + + +* **返回** + + + * `MessageFormatter[TM]`: 消息格式化器 + + + ### `append(obj)` @@ -499,3 +533,19 @@ Event 基类。提供获取关键信息的方法,其余信息可直接获取 * `bool` + + + +## _class_ `MessageFormatter` + +基类:`string.Formatter`, `Generic`[`nonebot.adapters._formatter.TM`] + +消息模板格式化实现类 + + +### `format(*args, **kwargs)` + + +* **说明** + + 根据模板和参数生成消息对象