1
1
import asyncio
2
2
import importlib
3
- from collections .abc import Iterable
3
+ from collections .abc import Generator , Iterable
4
4
from contextlib import suppress
5
5
from functools import lru_cache
6
6
from importlib .metadata import Distribution , PackageNotFoundError , distribution
7
- from typing import Optional
7
+ from pathlib import Path
8
8
9
9
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
11
11
from nonebot import logger
12
12
from nonebot .plugin import Plugin
13
13
14
+ from ..config import external_infos_dir , pm_menus_dir
14
15
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
17
18
18
19
19
20
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:
24
25
25
26
26
27
@lru_cache
27
- def get_dist (module_name : str ) -> Optional [ Distribution ] :
28
+ def get_dist (module_name : str ) -> Distribution | None :
28
29
with warning_suppress (f"Unexpected error happened when getting info of package { module_name } " ),\
29
30
suppress (PackageNotFoundError ): # fmt: skip
30
31
return distribution (module_name )
@@ -35,7 +36,7 @@ def get_dist(module_name: str) -> Optional[Distribution]:
35
36
36
37
37
38
@lru_cache
38
- def get_version_attr (module_name : str ) -> Optional [ str ] :
39
+ def get_version_attr (module_name : str ) -> str | None :
39
40
with warning_suppress (f"Unexpected error happened when importing { module_name } " ),\
40
41
suppress (ImportError ): # fmt: skip
41
42
m = importlib .import_module (module_name )
@@ -49,7 +50,7 @@ def get_version_attr(module_name: str) -> Optional[str]:
49
50
50
51
async def get_info_from_plugin (plugin : Plugin ) -> PMNPluginInfo :
51
52
meta = plugin .metadata
52
- extra : Optional [ PMNPluginExtra ] = None
53
+ extra : PMNPluginExtra | None = None
53
54
if meta :
54
55
with warning_suppress (f"Failed to parse plugin metadata of { plugin .id_ } " ):
55
56
extra = type_validate_python (PMNPluginExtra , meta .extra )
@@ -68,19 +69,25 @@ async def get_info_from_plugin(plugin: Plugin) -> PMNPluginInfo:
68
69
else None
69
70
)
70
71
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
+ ):
72
75
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" ])
75
82
):
76
83
author = normalize_metadata_user (author , allow_multi = True )
77
84
78
85
description = (
79
86
meta .description
80
87
if meta
81
88
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
84
91
else None
85
92
)
86
93
)
@@ -102,6 +109,107 @@ async def get_info_from_plugin(plugin: Plugin) -> PMNPluginInfo:
102
109
)
103
110
104
111
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
+
105
213
async def collect_plugin_infos (plugins : Iterable [Plugin ]):
106
214
async def _get (p : Plugin ):
107
215
with warning_suppress (f"Failed to get plugin info of { p .id_ } " ):
0 commit comments