Skip to content

Commit feb2408

Browse files
committed
up
1 parent dc1932f commit feb2408

File tree

11 files changed

+727
-609
lines changed

11 files changed

+727
-609
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
_✨ 新一代的图片帮助插件 ✨_
1616

17-
<img src="https://img.shields.io/badge/python-3.9+-blue.svg" alt="python">
17+
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python">
1818
<a href="https://pdm.fming.dev">
1919
<img src="https://img.shields.io/badge/pdm-managed-blueviolet" alt="pdm-managed">
2020
</a>

nonebot_plugin_picmenu_next/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from .data_source import refresh_infos
1212
from .templates import load_builtin_templates
1313

14-
__version__ = "0.1.3"
14+
__version__ = "0.1.4"
1515
__plugin_meta__ = PluginMetadata(
1616
name="PicMenu Next",
1717
description="新一代的图片帮助插件",

nonebot_plugin_picmenu_next/__main__.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from collections.abc import Sequence
22
from pathlib import Path
3-
from typing import Optional, TypeVar
3+
from typing import TypeVar
44

55
from arclet.alconna import Alconna, Arg, Args, CommandMeta, Option, store_true
66
from loguru import logger
@@ -81,7 +81,7 @@ def get_name_similarities(
8181
T = TypeVar("T")
8282

8383

84-
def handle_query_index(query: str, infos: Sequence[T]) -> Optional[tuple[int, T]]:
84+
def handle_query_index(query: str, infos: Sequence[T]) -> tuple[int, T] | None:
8585
if query.isdigit() and query.strip("0"):
8686
return (
8787
((i := qn - 1), infos[i])
@@ -95,7 +95,7 @@ async def query_plugin(
9595
infos: list[PMNPluginInfo],
9696
query: str,
9797
score_cutoff: float = 60,
98-
) -> Optional[tuple[int, PMNPluginInfo]]:
98+
) -> tuple[int, PMNPluginInfo] | None:
9999
if r := handle_query_index(query, infos):
100100
return r
101101

@@ -121,7 +121,7 @@ async def query_func_detail(
121121
pm_data: list[PMDataItem],
122122
query: str,
123123
score_cutoff: float = 60,
124-
) -> Optional[tuple[int, PMDataItem]]:
124+
) -> tuple[int, PMDataItem] | None:
125125
if r := handle_query_index(query, pm_data):
126126
return r
127127

@@ -147,8 +147,8 @@ async def query_func_detail(
147147
async def _(
148148
bot: BaseBot,
149149
ev: BaseEvent,
150-
q_plugin: Query[Optional[str]] = Query("~plugin", None),
151-
q_function: Query[Optional[str]] = Query("~function", None),
150+
q_plugin: Query[str | None] = Query("~plugin", None),
151+
q_function: Query[str | None] = Query("~function", None),
152152
q_show_hidden: Query[bool] = Query("~show-hidden.value", default=False),
153153
):
154154
show_hidden = q_show_hidden.result
@@ -159,7 +159,7 @@ async def _(
159159
):
160160
await (
161161
UniMessage.image(raw=TIP_IMG_PATH.read_bytes())
162-
.text("别想看咱藏起来的东西!")
162+
.text("不是主人不给看")
163163
.finish(reply_to=True)
164164
)
165165

nonebot_plugin_picmenu_next/config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1+
from pathlib import Path
2+
3+
from cookit.nonebot.localstore import ensure_localstore_path_config
14
from cookit.pyd import model_with_alias_generator
25
from nonebot import get_plugin_config
6+
from nonebot_plugin_localstore import get_plugin_config_dir
37
from pydantic import BaseModel
48

9+
ensure_localstore_path_config()
10+
11+
config_dir = get_plugin_config_dir()
12+
13+
pm_menus_dir = Path.cwd() / "menu_config/menus"
14+
external_infos_dir = config_dir / "external_infos"
15+
external_infos_dir.mkdir(parents=True, exist_ok=True)
16+
517

618
@model_with_alias_generator(lambda x: f"pmn_{x}")
719
class ConfigModel(BaseModel):

nonebot_plugin_picmenu_next/data_source/collect.py

Lines changed: 121 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
import asyncio
22
import importlib
3-
from collections.abc import Iterable
3+
from collections.abc import Generator, Iterable
44
from contextlib import suppress
55
from functools import lru_cache
66
from importlib.metadata import Distribution, PackageNotFoundError, distribution
7-
from typing import Optional
7+
from pathlib import Path
88

99
from cookit.loguru import warning_suppress
10-
from cookit.pyd import type_validate_python
10+
from cookit.pyd import type_validate_json, type_validate_python
1111
from nonebot import logger
1212
from nonebot.plugin import Plugin
1313

14+
from ..config import external_infos_dir, pm_menus_dir
1415
from ..utils import normalize_plugin_name
15-
from .mixin import chain_mixins, plugin_collect_mixins
16-
from .models import PMNData, PMNPluginExtra, PMNPluginInfo
16+
from .mixin import PluginCollectMixinNext, chain_mixins, plugin_collect_mixins
17+
from .models import ExternalPluginInfo, PMNData, PMNPluginExtra, PMNPluginInfo
1718

1819

1920
def normalize_metadata_user(info: str, allow_multi: bool = False) -> str:
@@ -24,7 +25,7 @@ def normalize_metadata_user(info: str, allow_multi: bool = False) -> str:
2425

2526

2627
@lru_cache
27-
def get_dist(module_name: str) -> Optional[Distribution]:
28+
def get_dist(module_name: str) -> Distribution | None:
2829
with warning_suppress(f"Unexpected error happened when getting info of package {module_name}"),\
2930
suppress(PackageNotFoundError): # fmt: skip
3031
return distribution(module_name)
@@ -35,7 +36,7 @@ def get_dist(module_name: str) -> Optional[Distribution]:
3536

3637

3738
@lru_cache
38-
def get_version_attr(module_name: str) -> Optional[str]:
39+
def get_version_attr(module_name: str) -> str | None:
3940
with warning_suppress(f"Unexpected error happened when importing {module_name}"),\
4041
suppress(ImportError): # fmt: skip
4142
m = importlib.import_module(module_name)
@@ -49,7 +50,7 @@ def get_version_attr(module_name: str) -> Optional[str]:
4950

5051
async def get_info_from_plugin(plugin: Plugin) -> PMNPluginInfo:
5152
meta = plugin.metadata
52-
extra: Optional[PMNPluginExtra] = None
53+
extra: PMNPluginExtra | None = None
5354
if meta:
5455
with warning_suppress(f"Failed to parse plugin metadata of {plugin.id_}"):
5556
extra = type_validate_python(PMNPluginExtra, meta.extra)
@@ -68,19 +69,25 @@ async def get_info_from_plugin(plugin: Plugin) -> PMNPluginInfo:
6869
else None
6970
)
7071
if not author and (dist := get_dist(plugin.module_name)):
71-
if author := dist.metadata.get("Author") or dist.metadata.get("Maintainer"):
72+
if (("Author" in dist.metadata) and (author := dist.metadata["Author"])) or (
73+
("Maintainer" in dist.metadata) and (author := dist.metadata["Maintainer"])
74+
):
7275
author = normalize_metadata_user(author)
73-
elif author := dist.metadata.get("Author-Email") or dist.metadata.get(
74-
"Maintainer-Email",
76+
elif (
77+
("Author-Email" in dist.metadata)
78+
and (author := dist.metadata["Author-Email"])
79+
) or (
80+
("Maintainer-Email" in dist.metadata)
81+
and (author := dist.metadata["Maintainer-Email"])
7582
):
7683
author = normalize_metadata_user(author, allow_multi=True)
7784

7885
description = (
7986
meta.description
8087
if meta
8188
else (
82-
dist.metadata.get("Summary")
83-
if (dist := get_dist(plugin.module_name))
89+
dist.metadata["Summary"]
90+
if (dist := get_dist(plugin.module_name)) and "Summary" in dist.metadata
8491
else None
8592
)
8693
)
@@ -102,6 +109,107 @@ async def get_info_from_plugin(plugin: Plugin) -> PMNPluginInfo:
102109
)
103110

104111

112+
def scan_path(path: Path, suffixes: Iterable[str] | None = None) -> Generator[Path]:
113+
for child in path.iterdir():
114+
if child.is_dir():
115+
yield from scan_path(child, suffixes)
116+
elif suffixes and child.suffix in suffixes:
117+
yield child
118+
119+
120+
async def collect_menus():
121+
yaml = None
122+
123+
supported_suffixes = {".json", ".yml", ".yaml", ".toml"}
124+
125+
def _load_file(path: Path) -> ExternalPluginInfo:
126+
nonlocal yaml
127+
128+
if path.suffix == ".json":
129+
return type_validate_json(ExternalPluginInfo, path.read_text("u8"))
130+
131+
if path.suffix in {".yml", ".yaml"}:
132+
if not yaml:
133+
try:
134+
from ruamel.yaml import YAML
135+
except ImportError as e:
136+
raise ImportError(
137+
"Missing dependency for parsing yaml files, please install using"
138+
" `pip install nonebot-plugin-picmenu-next[yaml]`",
139+
) from e
140+
yaml = YAML()
141+
return type_validate_python(
142+
ExternalPluginInfo,
143+
yaml.load(path.read_text("u8")),
144+
)
145+
146+
if path.suffix == ".toml":
147+
try:
148+
import tomllib
149+
except ImportError:
150+
try:
151+
import tomli as tomllib
152+
except ImportError as e:
153+
raise ImportError(
154+
"Missing dependency for parsing toml files, please install using"
155+
" `pip install nonebot-plugin-picmenu-next[toml]`",
156+
) from e
157+
return type_validate_python(
158+
ExternalPluginInfo,
159+
tomllib.loads(path.read_text("u8")),
160+
)
161+
162+
raise ValueError("Unsupported file type")
163+
164+
infos: dict[str, ExternalPluginInfo] = {}
165+
166+
def _load_to_infos(path: Path):
167+
if path.name in infos:
168+
logger.warning(
169+
f"Find file with duplicated name `{path.name}`! Skip loading {{path}}",
170+
)
171+
return
172+
with warning_suppress(f"Failed to load file {path}"):
173+
infos[path.name] = _load_file(path)
174+
175+
def _load_all(path: Path):
176+
for x in scan_path(path, supported_suffixes):
177+
_load_to_infos(x)
178+
179+
if pm_menus_dir.exists():
180+
logger.warning(
181+
"Old PicMenu menus dir is deprecated"
182+
", recommended to migrate to PicMenu Next config dir",
183+
)
184+
_load_all(pm_menus_dir)
185+
186+
_load_all(external_infos_dir)
187+
188+
return infos
189+
190+
191+
@plugin_collect_mixins(priority=1)
192+
async def load_user_custom_infos_mixin(
193+
next_mixin: PluginCollectMixinNext,
194+
infos: list[PMNPluginInfo],
195+
) -> list[PMNPluginInfo]:
196+
external_infos = await collect_menus()
197+
if not external_infos:
198+
return await next_mixin(infos)
199+
logger.info(f"Collected {len(external_infos)} external infos")
200+
201+
infos_map = {x.plugin_id: x for x in infos if x.plugin_id}
202+
for k, v in external_infos.items():
203+
if k in infos_map:
204+
logger.debug(f"Found `{k}` in infos, will merge to original")
205+
v.merge_to(infos_map[k], plugin_id=k, copy=False)
206+
else:
207+
logger.debug(f"Not found `{k}` in infos, will add into")
208+
infos.append(v.to_plugin_info(k))
209+
210+
return await next_mixin(infos)
211+
212+
105213
async def collect_plugin_infos(plugins: Iterable[Plugin]):
106214
async def _get(p: Plugin):
107215
with warning_suppress(f"Failed to get plugin info of {p.id_}"):

nonebot_plugin_picmenu_next/data_source/mixin.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from collections import defaultdict
2-
from collections.abc import Coroutine, Sequence
2+
from collections.abc import Callable, Coroutine, Sequence
33
from dataclasses import dataclass
4-
from typing import Any, Callable, Generic, Optional, TypeVar
5-
from typing_extensions import Concatenate, ParamSpec, TypeAlias
4+
from typing import Any, Concatenate, Generic, TypeAlias, TypeVar
5+
from typing_extensions import ParamSpec
66

77
from cookit import DecoListCollector
88
from cookit.loguru import warning_suppress
@@ -67,15 +67,15 @@
6767
class MixinInfo(Generic[T]):
6868
func: T
6969
priority: int
70-
source: Optional[MatcherSource]
70+
source: MatcherSource | None
7171

7272

7373
class MixinCollector(DecoListCollector[MixinInfo[T]]):
7474
def __call__( # pyright: ignore[reportIncompatibleMethodOverride]
7575
self,
7676
priority: int = 5,
7777
_depth: int = 0,
78-
_matcher_source: Optional[MatcherSource] = None,
78+
_matcher_source: MatcherSource | None = None,
7979
):
8080
def deco(func: T) -> T:
8181
self.data.append(
@@ -99,7 +99,7 @@ def __call__(
9999
self,
100100
priority: int = 1,
101101
_depth: int = 0,
102-
_matcher_source: Optional[MatcherSource] = None,
102+
_matcher_source: MatcherSource | None = None,
103103
):
104104
def deco(f: T) -> T:
105105
s = _matcher_source or get_matcher_source()

0 commit comments

Comments
 (0)