Skip to content

Commit df45e19

Browse files
authored
Feature: 添加获取当前已安装适配器和插件的功能支持 (#171)
* ✨ add functions to list installed adapters and plugins * 🐛 fix ambiguous error message for `find_exact_package` when `packages` is empty * ♻️ re-export functions for listing installed adapters and plugins * ✨ integrate list_installed_adapters/plugins into cli * 🌐 add translations for new messages
1 parent 41c0db8 commit df45e19

File tree

8 files changed

+323
-160
lines changed

8 files changed

+323
-160
lines changed

nb_cli/cli/commands/adapter.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from nb_cli import _
88
from nb_cli.config import GLOBAL_CONFIG
9+
from nb_cli.exceptions import NoSelectablePackageError
910
from nb_cli.cli.utils import find_exact_package, format_package_results
1011
from nb_cli.cli import (
1112
CLI_DEFAULT_STYLE,
@@ -21,6 +22,7 @@
2122
call_pip_update,
2223
call_pip_install,
2324
call_pip_uninstall,
25+
list_installed_adapters,
2426
)
2527

2628

@@ -80,6 +82,13 @@ async def store():
8082
@adapter.command(
8183
name="list", help=_("List nonebot adapters published on nonebot homepage.")
8284
)
85+
@click.option(
86+
"--installed",
87+
is_flag=True,
88+
default=False,
89+
flag_value=True,
90+
help=_("Whether to list installed adapters only in current project."),
91+
)
8392
@click.option(
8493
"--include-unpublished",
8594
is_flag=True,
@@ -88,8 +97,12 @@ async def store():
8897
help=_("Whether to include unpublished adapters."),
8998
)
9099
@run_async
91-
async def list_(include_unpublished: bool = False):
92-
adapters = await list_adapters(include_unpublished=include_unpublished)
100+
async def list_(installed: bool = False, include_unpublished: bool = False):
101+
adapters = (
102+
await list_installed_adapters()
103+
if installed
104+
else await list_adapters(include_unpublished=include_unpublished)
105+
)
93106
if include_unpublished:
94107
click.secho(_("WARNING: Unpublished adapters may be included."), fg="yellow")
95108
click.echo(format_package_results(adapters))
@@ -143,13 +156,23 @@ async def install(
143156
include_unpublished: bool = False,
144157
):
145158
try:
159+
_installed = {
160+
(a.project_link, a.module_name) for a in await list_installed_adapters()
161+
}
146162
adapter = await find_exact_package(
147163
_("Adapter name to install:"),
148164
name,
149-
await list_adapters(include_unpublished=include_unpublished),
165+
[
166+
a
167+
for a in await list_adapters(include_unpublished=include_unpublished)
168+
if (a.project_link, a.module_name) not in _installed
169+
],
150170
)
151171
except CancelledError:
152172
return
173+
except NoSelectablePackageError:
174+
click.echo(_("No available adapter found to install."))
175+
return
153176

154177
if include_unpublished:
155178
click.secho(
@@ -216,10 +239,13 @@ async def update(
216239
adapter = await find_exact_package(
217240
_("Adapter name to update:"),
218241
name,
219-
await list_adapters(include_unpublished=include_unpublished),
242+
await list_installed_adapters(),
220243
)
221244
except CancelledError:
222245
return
246+
except NoSelectablePackageError:
247+
click.echo(_("No installed adapter found to update."))
248+
return
223249

224250
if include_unpublished:
225251
click.secho(
@@ -263,12 +289,13 @@ async def uninstall(name: str | None, pip_args: list[str] | None):
263289
adapter = await find_exact_package(
264290
_("Adapter name to uninstall:"),
265291
name,
266-
await list_adapters(
267-
include_unpublished=True # unpublished modules are always removable
268-
),
292+
await list_installed_adapters(),
269293
)
270294
except CancelledError:
271295
return
296+
except NoSelectablePackageError:
297+
click.echo(_("No installed adapter found to uninstall."))
298+
return
272299

273300
try:
274301
can_uninstall = GLOBAL_CONFIG.remove_adapter(adapter)

nb_cli/cli/commands/plugin.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from nb_cli import _
88
from nb_cli.config import GLOBAL_CONFIG
9+
from nb_cli.exceptions import NoSelectablePackageError
910
from nb_cli.cli.utils import find_exact_package, format_package_results
1011
from nb_cli.cli import (
1112
CLI_DEFAULT_STYLE,
@@ -21,6 +22,7 @@
2122
call_pip_update,
2223
call_pip_install,
2324
call_pip_uninstall,
25+
list_installed_plugins,
2426
)
2527

2628

@@ -80,6 +82,13 @@ async def store():
8082
@plugin.command(
8183
name="list", help=_("List nonebot plugins published on nonebot homepage.")
8284
)
85+
@click.option(
86+
"--installed",
87+
is_flag=True,
88+
default=False,
89+
flag_value=True,
90+
help=_("Whether to list installed plugins only in current project."),
91+
)
8392
@click.option(
8493
"--include-unpublished",
8594
is_flag=True,
@@ -88,8 +97,12 @@ async def store():
8897
help=_("Whether to include unpublished plugins."),
8998
)
9099
@run_async
91-
async def list_(include_unpublished: bool = False):
92-
plugins = await list_plugins(include_unpublished=include_unpublished)
100+
async def list_(installed: bool = False, include_unpublished: bool = False):
101+
plugins = (
102+
await list_installed_plugins()
103+
if installed
104+
else await list_plugins(include_unpublished=include_unpublished)
105+
)
93106
if include_unpublished:
94107
click.secho(_("WARNING: Unpublished plugins may be included."), fg="yellow")
95108
click.echo(format_package_results(plugins))
@@ -143,13 +156,23 @@ async def install(
143156
include_unpublished: bool = False,
144157
):
145158
try:
159+
_installed = {
160+
(p.project_link, p.module_name) for p in await list_installed_plugins()
161+
}
146162
plugin = await find_exact_package(
147163
_("Plugin name to install:"),
148164
name,
149-
await list_plugins(include_unpublished=include_unpublished),
165+
[
166+
p
167+
for p in await list_plugins(include_unpublished=include_unpublished)
168+
if (p.project_link, p.module_name) not in _installed
169+
],
150170
)
151171
except CancelledError:
152172
return
173+
except NoSelectablePackageError:
174+
click.echo(_("No available plugin found to install."))
175+
return
153176

154177
if include_unpublished:
155178
click.secho(
@@ -216,10 +239,13 @@ async def update(
216239
plugin = await find_exact_package(
217240
_("Plugin name to update:"),
218241
name,
219-
await list_plugins(include_unpublished=include_unpublished),
242+
await list_installed_plugins(),
220243
)
221244
except CancelledError:
222245
return
246+
except NoSelectablePackageError:
247+
click.echo(_("No installed plugin found to update."))
248+
return
223249

224250
if include_unpublished:
225251
click.secho(
@@ -263,12 +289,13 @@ async def uninstall(name: str | None, pip_args: list[str] | None):
263289
plugin = await find_exact_package(
264290
_("Plugin name to uninstall:"),
265291
name,
266-
await list_plugins(
267-
include_unpublished=True # unpublished modules are always removable
268-
),
292+
await list_installed_plugins(),
269293
)
270294
except CancelledError:
271295
return
296+
except NoSelectablePackageError:
297+
click.echo(_("No installed plugin found to uninstall."))
298+
return
272299

273300
try:
274301
can_uninstall = GLOBAL_CONFIG.remove_plugin(plugin)

nb_cli/cli/utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from nb_cli import _
1616
from nb_cli.config import Driver, Plugin, Adapter
17+
from nb_cli.exceptions import NoSelectablePackageError
1718

1819
T = TypeVar("T", Adapter, Plugin, Driver)
1920
P = ParamSpec("P")
@@ -59,6 +60,8 @@ def __call__(self, x: Adapter | Plugin | Driver, *, value: str) -> bool: ...
5960

6061
async def find_exact_package(question: str, name: str | None, packages: list[T]) -> T:
6162
if name is None:
63+
if not packages:
64+
raise NoSelectablePackageError("No packages available to select.")
6265
return (
6366
await ListPrompt(
6467
question,

nb_cli/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,7 @@ class ProjectInvalidError(RuntimeError):
2424

2525
class LocalCacheExpired(RuntimeError):
2626
"""Raised when local metadata cache is outdated."""
27+
28+
29+
class NoSelectablePackageError(RuntimeError):
30+
"""Raised when there is no selectable package."""

nb_cli/handlers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,14 @@
7777
from .plugin import list_plugins as list_plugins
7878
from .plugin import create_plugin as create_plugin
7979
from .plugin import list_builtin_plugins as list_builtin_plugins
80+
from .plugin import list_installed_plugins as list_installed_plugins
8081

8182
# isort: split
8283

8384
# adapter
8485
from .adapter import list_adapters as list_adapters
8586
from .adapter import create_adapter as create_adapter
87+
from .adapter import list_installed_adapters as list_installed_adapters
8688

8789
# isort: split
8890

nb_cli/handlers/adapter.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
from cookiecutter.main import cookiecutter
44

5-
from nb_cli.config import Adapter
65
from nb_cli.compat import model_dump
6+
from nb_cli.exceptions import ProjectInvalidError
7+
from nb_cli.config import Adapter, NoneBotConfig, LegacyNoneBotConfig
78

9+
from .meta import get_nonebot_config, requires_project_root
810
from .store import load_module_data, load_unpublished_modules
911

1012
TEMPLATE_ROOT = Path(__file__).parent.parent / "template" / "adapter"
@@ -42,3 +44,34 @@ async def list_adapters(
4244
).values()
4345
)
4446
]
47+
48+
49+
@requires_project_root
50+
async def list_installed_adapters(*, cwd: Path | None = None) -> list[Adapter]:
51+
config_data = get_nonebot_config(cwd)
52+
adapters = await load_module_data("adapter") + await load_unpublished_modules(
53+
Adapter
54+
)
55+
56+
result: list[Adapter] = []
57+
58+
if isinstance(config_data, NoneBotConfig):
59+
adapter_info = config_data.adapters
60+
allowed_pairs = {
61+
(pkg_name, m.module_name)
62+
for pkg_name, modules in adapter_info.items()
63+
for m in modules
64+
}
65+
for adapter in adapters:
66+
if (adapter.project_link, adapter.module_name) in allowed_pairs:
67+
result.append(adapter)
68+
elif isinstance(config_data, LegacyNoneBotConfig):
69+
adapter_info = config_data.adapters
70+
allowed_pairs = {m.module_name for m in adapter_info}
71+
for adapter in adapters:
72+
if adapter.module_name in allowed_pairs:
73+
result.append(adapter)
74+
else:
75+
raise ProjectInvalidError("Invalid config data type")
76+
77+
return result

nb_cli/handlers/plugin.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@
44

55
from cookiecutter.main import cookiecutter
66

7-
from nb_cli.config import Plugin
87
from nb_cli.compat import model_dump
8+
from nb_cli.exceptions import ProjectInvalidError
9+
from nb_cli.config import Plugin, NoneBotConfig, LegacyNoneBotConfig
910

1011
from . import templates
1112
from .process import create_process
12-
from .meta import requires_nonebot, get_default_python
1313
from .store import load_module_data, load_unpublished_modules
14+
from .meta import (
15+
requires_nonebot,
16+
get_default_python,
17+
get_nonebot_config,
18+
requires_project_root,
19+
)
1420

1521
TEMPLATE_ROOT = Path(__file__).parent.parent / "template" / "plugin"
1622

@@ -67,3 +73,32 @@ async def list_plugins(
6773
).values()
6874
)
6975
]
76+
77+
78+
@requires_project_root
79+
async def list_installed_plugins(*, cwd: Path | None = None) -> list[Plugin]:
80+
config_data = get_nonebot_config(cwd)
81+
plugins = await load_module_data("plugin") + await load_unpublished_modules(Plugin)
82+
83+
result: list[Plugin] = []
84+
85+
if isinstance(config_data, NoneBotConfig):
86+
plugin_info = config_data.plugins
87+
allowed_plugins = {
88+
(pkg_name, module_name)
89+
for pkg_name, module_names in plugin_info.items()
90+
for module_name in module_names
91+
}
92+
for plugin in plugins:
93+
if (plugin.project_link, plugin.module_name) in allowed_plugins:
94+
result.append(plugin)
95+
elif isinstance(config_data, LegacyNoneBotConfig):
96+
plugin_info = config_data.plugins
97+
allowed_plugins = set(plugin_info)
98+
for plugin in plugins:
99+
if plugin.module_name in allowed_plugins:
100+
result.append(plugin)
101+
else:
102+
raise ProjectInvalidError("Invalid config data type")
103+
104+
return result

0 commit comments

Comments
 (0)