diff --git a/nb_cli/cli/commands/adapter.py b/nb_cli/cli/commands/adapter.py index fc299d76..de051a16 100644 --- a/nb_cli/cli/commands/adapter.py +++ b/nb_cli/cli/commands/adapter.py @@ -7,8 +7,8 @@ from nb_cli import _ from nb_cli.config import GLOBAL_CONFIG -from nb_cli.exceptions import NoSelectablePackageError from nb_cli.cli.utils import find_exact_package, format_package_results +from nb_cli.exceptions import ProcessExecutionError, NoSelectablePackageError from nb_cli.cli import ( CLI_DEFAULT_STYLE, ClickAliasedGroup, @@ -18,11 +18,9 @@ run_async, ) from nb_cli.handlers import ( + EnvironmentExecutor, list_adapters, create_adapter, - call_pip_update, - call_pip_install, - call_pip_uninstall, list_installed_adapters, ) @@ -205,11 +203,13 @@ async def install( fg="yellow", ) - proc = await call_pip_install( - adapter.as_dependency(extras=extras, versioned=not no_restrict_version), - pip_args, - ) - if await proc.wait() != 0: + executor = await EnvironmentExecutor.get() + try: + await executor.install( + adapter.as_requirement(extras=extras, versioned=not no_restrict_version), + extra_args=pip_args or (), + ) + except ProcessExecutionError: click.secho( _( "Errors occurred in installing adapter {adapter.name}\n" @@ -218,8 +218,7 @@ async def install( ).format(adapter=adapter), fg="red", ) - assert proc.returncode - ctx.exit(proc.returncode) + ctx.exit(1) try: GLOBAL_CONFIG.add_adapter(adapter) @@ -230,17 +229,6 @@ async def install( ) ) - try: - GLOBAL_CONFIG.add_dependency( - adapter.as_dependency(extras=extras, versioned=not no_restrict_version) - ) - except RuntimeError as e: - click.echo( - _("Failed to add adapter {adapter.name} to dependencies: {e}").format( - adapter=adapter, e=e - ) - ) - @adapter.command( context_settings={"ignore_unknown_options": True}, help=_("Update nonebot adapter.") @@ -281,8 +269,13 @@ async def update( fg="yellow", ) - proc = await call_pip_update(adapter.project_link, pip_args) - if await proc.wait() != 0: + executor = await EnvironmentExecutor.get() + try: + await executor.install( + adapter.as_requirement(versioned=False), + extra_args=pip_args or (), + ) + except ProcessExecutionError: click.secho( _("Errors occurred in updating adapter {adapter.name}. Aborted.").format( adapter=adapter @@ -291,15 +284,6 @@ async def update( ) return - try: - GLOBAL_CONFIG.update_dependency(adapter) - except RuntimeError as e: - click.echo( - _("Failed to update adapter {adapter.name} to dependencies: {e}").format( - adapter=adapter, e=e - ) - ) - @adapter.command( aliases=["remove"], @@ -310,6 +294,11 @@ async def update( @click.argument("pip_args", nargs=-1, default=None) @run_async async def uninstall(name: str | None, pip_args: list[str] | None): + extras: str | None = None + if name and "[" in name: + name, extras = name.split("[", 1) + extras = extras.rstrip("]") + try: adapter = await find_exact_package( _("Adapter name to uninstall:"), @@ -322,20 +311,8 @@ async def uninstall(name: str | None, pip_args: list[str] | None): click.echo(_("No installed adapter found to uninstall.")) return - extras: str | None = None - if name and "[" in name: - name, extras = name.split("[", 1) - extras = extras.rstrip("]") - try: - if extras is not None: - if not GLOBAL_CONFIG.remove_dependency( - adapter.as_dependency(extras=extras) - ): - return can_uninstall = GLOBAL_CONFIG.remove_adapter(adapter) - if can_uninstall: - GLOBAL_CONFIG.remove_dependency(adapter) except RuntimeError as e: click.echo( _("Failed to remove adapter {adapter.name} from config: {e}").format( @@ -345,8 +322,11 @@ async def uninstall(name: str | None, pip_args: list[str] | None): return if can_uninstall: - proc = await call_pip_uninstall(adapter.project_link, pip_args) - await proc.wait() + executor = await EnvironmentExecutor.get() + await executor.uninstall( + adapter.as_requirement(extras=extras, versioned=False), + extra_args=pip_args or (), + ) @adapter.command(aliases=["new"], help=_("Create a new nonebot adapter.")) diff --git a/nb_cli/cli/commands/driver.py b/nb_cli/cli/commands/driver.py index 390c46db..158b4fa2 100644 --- a/nb_cli/cli/commands/driver.py +++ b/nb_cli/cli/commands/driver.py @@ -1,17 +1,13 @@ from typing import cast import click +from packaging.requirements import Requirement from noneprompt import Choice, ListPrompt, InputPrompt, CancelledError from nb_cli import _ -from nb_cli.config import GLOBAL_CONFIG +from nb_cli.exceptions import ProcessExecutionError +from nb_cli.handlers import EnvironmentExecutor, list_drivers from nb_cli.cli.utils import find_exact_package, format_package_results -from nb_cli.handlers import ( - list_drivers, - call_pip_update, - call_pip_install, - call_pip_uninstall, -) from nb_cli.cli import ( CLI_DEFAULT_STYLE, ClickAliasedGroup, @@ -153,25 +149,20 @@ async def install( fg="yellow", ) - if driver.project_link: - proc = await call_pip_install(driver.project_link, pip_args) - if await proc.wait() != 0: - click.secho( - _( - "Errors occurred in installing driver {driver.name}. Aborted." - ).format(driver=driver), - fg="red", - ) - return - + executor = await EnvironmentExecutor.get() try: - GLOBAL_CONFIG.add_dependency(driver) - except RuntimeError as e: - click.echo( - _("Failed to add driver {driver.name} to dependencies: {e}").format( - driver=driver, e=e - ) + await executor.install( + driver.as_requirement() if driver.project_link else Requirement("nonebot2"), + extra_args=pip_args or (), ) + except ProcessExecutionError: + click.secho( + _("Errors occurred in installing driver {driver.name}. Aborted.").format( + driver=driver + ), + fg="red", + ) + return @driver.command( @@ -210,25 +201,20 @@ async def update( fg="yellow", ) - if driver.project_link: - proc = await call_pip_update(driver.project_link, pip_args) - if await proc.wait() != 0: - click.secho( - _("Errors occurred in updating driver {driver.name}. Aborted.").format( - driver=driver - ), - fg="red", - ) - return - + executor = await EnvironmentExecutor.get() try: - GLOBAL_CONFIG.update_dependency(driver) - except RuntimeError as e: - click.echo( - _("Failed to update driver {driver.name} to dependencies: {e}").format( - driver=driver, e=e - ) + await executor.install( + driver.as_requirement() if driver.project_link else Requirement("nonebot2"), + extra_args=pip_args or (), + ) + except ProcessExecutionError: + click.secho( + _("Errors occurred in updating driver {driver.name}. Aborted.").format( + driver=driver + ), + fg="red", ) + return @driver.command( @@ -251,19 +237,9 @@ async def uninstall(name: str | None, pip_args: list[str] | None): except CancelledError: return - try: - GLOBAL_CONFIG.remove_dependency(driver) - GLOBAL_CONFIG.add_dependency("nonebot2") # hold a nonebot2 package - except RuntimeError as e: - click.echo( - _("Failed to remove driver {driver.name} from dependencies: {e}").format( - driver=driver, e=e - ) + if driver.project_link: + executor = await EnvironmentExecutor.get() + await executor.uninstall( + driver.as_requirement(versioned=False), extra_args=pip_args or () ) - - if package := driver.project_link: - if package.startswith("nonebot2[") and package.endswith("]"): - package = package[9:-1] - - proc = await call_pip_uninstall(package, pip_args) - await proc.wait() + await executor.install(Requirement("nonebot2"), extra_args=pip_args or ()) diff --git a/nb_cli/cli/commands/plugin.py b/nb_cli/cli/commands/plugin.py index ec470035..b55454f0 100644 --- a/nb_cli/cli/commands/plugin.py +++ b/nb_cli/cli/commands/plugin.py @@ -7,8 +7,14 @@ from nb_cli import _ from nb_cli.config import GLOBAL_CONFIG -from nb_cli.exceptions import NoSelectablePackageError from nb_cli.cli.utils import find_exact_package, format_package_results +from nb_cli.exceptions import ProcessExecutionError, NoSelectablePackageError +from nb_cli.handlers import ( + EnvironmentExecutor, + list_plugins, + create_plugin, + list_installed_plugins, +) from nb_cli.cli import ( CLI_DEFAULT_STYLE, ClickAliasedGroup, @@ -17,14 +23,6 @@ run_sync, run_async, ) -from nb_cli.handlers import ( - list_plugins, - create_plugin, - call_pip_update, - call_pip_install, - call_pip_uninstall, - list_installed_plugins, -) @click.group( @@ -203,10 +201,13 @@ async def install( fg="yellow", ) - proc = await call_pip_install( - plugin.as_dependency(extras=extras, versioned=not no_restrict_version), pip_args - ) - if await proc.wait() != 0: + executor = await EnvironmentExecutor.get() + try: + await executor.install( + plugin.as_requirement(extras=extras, versioned=not no_restrict_version), + extra_args=pip_args or (), + ) + except ProcessExecutionError: click.secho( _( "Errors occurred in installing plugin {plugin.name}\n" @@ -215,8 +216,7 @@ async def install( ).format(plugin=plugin), fg="red", ) - assert proc.returncode - ctx.exit(proc.returncode) + ctx.exit(1) try: GLOBAL_CONFIG.add_plugin(plugin) @@ -227,17 +227,6 @@ async def install( ) ) - try: - GLOBAL_CONFIG.add_dependency( - plugin.as_dependency(extras=extras, versioned=not no_restrict_version) - ) - except RuntimeError as e: - click.echo( - _("Failed to add plugin {plugin.name} to dependencies: {e}").format( - plugin=plugin, e=e - ) - ) - @plugin.command( context_settings={"ignore_unknown_options": True}, help=_("Update nonebot plugin.") @@ -278,8 +267,10 @@ async def update( fg="yellow", ) - proc = await call_pip_update(plugin.project_link, pip_args) - if await proc.wait() != 0: + executor = await EnvironmentExecutor.get() + try: + await executor.update(plugin.as_requirement(), extra_args=pip_args or ()) + except ProcessExecutionError: click.secho( _("Errors occurred in updating plugin {plugin.name}. Aborted.").format( plugin=plugin @@ -288,15 +279,6 @@ async def update( ) return - try: - GLOBAL_CONFIG.update_dependency(plugin) - except RuntimeError as e: - click.echo( - _("Failed to update plugin {plugin.name} to dependencies: {e}").format( - plugin=plugin, e=e - ) - ) - @plugin.command( aliases=["remove"], @@ -307,6 +289,11 @@ async def update( @click.argument("pip_args", nargs=-1, default=None) @run_async async def uninstall(name: str | None, pip_args: list[str] | None): + extras: str | None = None + if name and "[" in name: + name, extras = name.split("[", 1) + extras = extras.rstrip("]") + try: plugin = await find_exact_package( _("Plugin name to uninstall:"), @@ -319,18 +306,8 @@ async def uninstall(name: str | None, pip_args: list[str] | None): click.echo(_("No installed plugin found to uninstall.")) return - extras: str | None = None - if name and "[" in name: - name, extras = name.split("[", 1) - extras = extras.rstrip("]") - try: - if extras is not None: - if not GLOBAL_CONFIG.remove_dependency(plugin.as_dependency(extras=extras)): - return can_uninstall = GLOBAL_CONFIG.remove_plugin(plugin) - if can_uninstall: - GLOBAL_CONFIG.remove_dependency(plugin) except RuntimeError as e: click.echo( _("Failed to remove plugin {plugin.name} from config: {e}").format( @@ -340,8 +317,11 @@ async def uninstall(name: str | None, pip_args: list[str] | None): return if can_uninstall: - proc = await call_pip_uninstall(plugin.project_link, pip_args) - await proc.wait() + executor = await EnvironmentExecutor.get() + await executor.uninstall( + plugin.as_requirement(extras=extras, versioned=False), + extra_args=pip_args or (), + ) @plugin.command(aliases=["new"], help=_("Create a new nonebot plugin.")) diff --git a/nb_cli/cli/commands/project.py b/nb_cli/cli/commands/project.py index f3db4adf..91b7e25d 100644 --- a/nb_cli/cli/commands/project.py +++ b/nb_cli/cli/commands/project.py @@ -12,6 +12,7 @@ import click import nonestorage +from packaging.requirements import Requirement from noneprompt import ( Choice, ListPrompt, @@ -27,17 +28,17 @@ from nb_cli.config import ConfigManager from nb_cli.consts import DEFAULT_DRIVER from nb_cli.cli.utils import advanced_search -from nb_cli.exceptions import ModuleLoadFailed +from nb_cli.exceptions import ModuleLoadFailed, ProcessExecutionError from nb_cli.cli import CLI_DEFAULT_STYLE, ClickAliasedCommand, run_async from nb_cli.handlers import ( Reloader, FileFilter, + EnvironmentExecutor, run_project, list_drivers, list_plugins, list_adapters, create_project, - call_pip_install, get_project_root, create_virtualenv, terminate_process, @@ -45,6 +46,7 @@ list_builtin_plugins, list_project_templates, upgrade_project_format, + all_environment_managers, downgrade_project_format, ) @@ -342,12 +344,23 @@ async def create( if install_dependencies: try: - use_venv = await ConfirmPrompt( - _("Create virtual environment?"), default_choice=True + manager = await ListPrompt( + _("Which project manager would you like to use?"), + choices=[Choice(mgr, mgr) for mgr in await all_environment_managers()], ).prompt_async(style=CLI_DEFAULT_STYLE) except CancelledError: return + if manager.data == "pip": + try: + use_venv = await ConfirmPrompt( + _("Create virtual environment?"), default_choice=True + ).prompt_async(style=CLI_DEFAULT_STYLE) + except CancelledError: + return + else: + use_venv = True + if use_venv: click.secho( _("Creating virtual environment in {venv_dir} ...").format( @@ -361,28 +374,59 @@ async def create( config_manager = ConfigManager(working_dir=project_dir, use_venv=use_venv) - proc = await call_pip_install( - ["nonebot2", *set(context.packages)], - pip_args, - python_path=config_manager.python_path, + executor = await EnvironmentExecutor.get( + manager.data, + toml_manager=config_manager, + cwd=project_dir, + executable=config_manager.python_path, ) - await proc.wait() + try: + await executor.install( + *(Requirement(pkg) for pkg in ["nonebot2", *set(context.packages)]), + extra_args=pip_args or (), + ) + except ProcessExecutionError: + click.secho( + _( + "Failed to install dependencies! " + "You should install the dependencies manually." + ), + fg="red", + ) + ctx.exit(1) - if proc.returncode == 0: - builtin_plugins = await list_builtin_plugins( - python_path=config_manager.python_path + try: + if context.variables.get("devtools"): + if await ConfirmPrompt( + _("Install developer dependencies?"), default_choice=True + ).prompt_async(style=CLI_DEFAULT_STYLE): + await executor.install( + *(Requirement(pkg) for pkg in context.variables["devtools"]), + extra_args=pip_args or (), + dev=True, + ) + except CancelledError: + pass + except ProcessExecutionError: + click.secho( + _( + "Failed to install developer dependencies! " + "You may install them manually." + ), + fg="red", ) - try: - loaded_builtin_plugins = [ - c.data - for c in await CheckboxPrompt( - _("Which builtin plugin(s) would you like to use?"), - [Choice(p, p) for p in builtin_plugins], - ).prompt_async(style=CLI_DEFAULT_STYLE) - ] - except CancelledError: - return + builtin_plugins = await list_builtin_plugins( + python_path=config_manager.python_path + ) + try: + loaded_builtin_plugins = [ + c.data + for c in await CheckboxPrompt( + _("Which builtin plugin(s) would you like to use?"), + [Choice(p, p) for p in builtin_plugins], + ).prompt_async(style=CLI_DEFAULT_STYLE) + ] try: for plugin in loaded_builtin_plugins: config_manager.add_builtin_plugin(plugin) @@ -393,48 +437,38 @@ async def create( ).format(builtin_plugin=loaded_builtin_plugins, e=e), fg="red", ) - ctx.exit(1) + except CancelledError: + pass - plugins = await list_plugins() - try: - pending_plugins = [ - c.data - for c in await CheckboxPrompt( - _("Which official plugins would you like to use?"), - [ - Choice(f"{p.name} ({p.desc})", p) - for p in advanced_search("#official", plugins) - ], - ).prompt_async(style=CLI_DEFAULT_STYLE) - ] - if pending_plugins: - proc = await call_pip_install( - [p.as_dependency() for p in pending_plugins], - pip_args, - python_path=config_manager.python_path, + plugins = await list_plugins() + try: + pending_plugins = [ + c.data + for c in await CheckboxPrompt( + _("Which official plugins would you like to use?"), + [ + Choice(f"{p.name} ({p.desc})", p) + for p in advanced_search("#official", plugins) + ], + ).prompt_async(style=CLI_DEFAULT_STYLE) + ] + if pending_plugins: + try: + await executor.install( + *(p.as_requirement() for p in pending_plugins), + extra_args=pip_args or (), ) - await proc.wait() - if proc.returncode == 0: - config_manager.add_dependency(*pending_plugins) config_manager.add_plugin(*pending_plugins) - else: + except ProcessExecutionError: click.secho( _( "Failed to install plugins! " - "You should install the plugins manually." + "You may install the plugins manually." ), fg="red", ) - except CancelledError: - return - else: - click.secho( - _( - "Failed to install dependencies! " - "You should install the dependencies manually." - ), - fg="red", - ) + except CancelledError: + pass click.secho(_("Done!"), fg="green") click.secho(_("Run the following command to start your bot:"), fg="green") diff --git a/nb_cli/config/model.py b/nb_cli/config/model.py index 34f67382..234c628f 100644 --- a/nb_cli/config/model.py +++ b/nb_cli/config/model.py @@ -4,6 +4,7 @@ from datetime import datetime from pydantic import BaseModel +from packaging.requirements import Requirement from nb_cli.compat import PYDANTIC_V2, ConfigDict @@ -52,6 +53,11 @@ def as_dependency( f"{self.project_link}>={self.version}" if versioned else self.project_link ) + def as_requirement( + self, *, extras: str | None = None, versioned: bool = True + ) -> Requirement: + return Requirement(self.as_dependency(extras=extras, versioned=versioned)) + class Adapter(PackageInfo): __module_name__ = "adapters" diff --git a/nb_cli/config/parser.py b/nb_cli/config/parser.py index 3b6d1c0a..d5439101 100644 --- a/nb_cli/config/parser.py +++ b/nb_cli/config/parser.py @@ -324,22 +324,37 @@ def update_nonebot_config( self._write_data(data) self._policy = self._select_policy() # update access policy - def get_dependencies(self) -> list[Requirement]: + def get_dependencies(self, *, group: str | None = None) -> list[Requirement]: data = self._get_data() - deps: list[str] = data.setdefault("project", {}).setdefault("dependencies", []) + if group is None: + deps: list[str] = data.setdefault("project", {}).setdefault( + "dependencies", [] + ) + else: + deps: list[str] = ( + data.setdefault("project", {}) + .setdefault("dependency-groups", {}) + .setdefault(group, []) + ) return [Requirement(d) for d in deps] - def add_dependency(self, *dependencies: str | PackageInfo) -> None: + def add_dependency( + self, *dependencies: str | PackageInfo | Requirement, group: str | None = None + ) -> None: if not dependencies: return - deps = self.get_dependencies() + deps = self.get_dependencies(group=group) with self._data_context("project", dict[str, Any]()) as project: for dependency in dependencies: - depinfo = Requirement( - dependency - if isinstance(dependency, str) - else dependency.as_dependency(versioned=False) + depinfo = ( + Requirement( + dependency + if isinstance(dependency, str) + else dependency.as_dependency(versioned=False) + ) + if isinstance(dependency, (str, PackageInfo)) + else dependency ) matches = [i for i, d in enumerate(deps) if d.name == depinfo.name] @@ -354,24 +369,38 @@ def add_dependency(self, *dependencies: str | PackageInfo) -> None: deps[idx] = _merge_package_requirements(deps[idx], depinfo) else: - depinfo = Requirement( - dependency - if isinstance(dependency, str) - else dependency.as_dependency(versioned=True) + depinfo = ( + Requirement( + dependency + if isinstance(dependency, str) + else dependency.as_dependency(versioned=True) + ) + if isinstance(dependency, (str, PackageInfo)) + else dependency ) deps.append(depinfo) - project["dependencies"] = tomlkit.array().multiline(True) - project["dependencies"].extend(str(d) for d in deps) + if group is None: + project["dependencies"] = tomlkit.array().multiline(True) + project["dependencies"].extend(str(d) for d in deps) + else: + project["dependency-groups"] = tomlkit.table() + gdep = tomlkit.array().multiline(True) + gdep.extend(str(d) for d in deps) + project["dependency-groups"].add(group, gdep) - def update_dependency(self, *dependencies: PackageInfo) -> None: + def update_dependency(self, *dependencies: PackageInfo | Requirement) -> None: if not dependencies: return deps = self.get_dependencies() with self._data_context("project", dict[str, Any]()) as project: for dependency in dependencies: - depinfo = Requirement(dependency.as_dependency(versioned=True)) + depinfo = ( + Requirement(dependency.as_dependency(versioned=True)) + if isinstance(dependency, PackageInfo) + else dependency + ) matches = [i for i, d in enumerate(deps) if d.name == depinfo.name] if matches: @@ -390,25 +419,32 @@ def update_dependency(self, *dependencies: PackageInfo) -> None: project["dependencies"] = tomlkit.array().multiline(True) project["dependencies"].extend(str(d) for d in deps) - def remove_dependency(self, *dependencies: str | PackageInfo) -> bool: + def remove_dependency( + self, *dependencies: str | PackageInfo | Requirement + ) -> list[Requirement]: """ 删除依赖记录操作。 Returns: - bool: 表示相关依赖是否全部被成功移除。 + list[Requirement]: 成功完整移除的相关依赖。 """ if not dependencies: - return False + return [] - status = True + removables: list[Requirement] = [] deps = self.get_dependencies() with self._data_context("project", dict[str, Any]()) as project: for dependency in dependencies: - depinfo = Requirement( - dependency - if isinstance(dependency, str) - else dependency.as_dependency(versioned=False) + status = True + depinfo = ( + Requirement( + dependency + if isinstance(dependency, str) + else dependency.as_dependency(versioned=False) + ) + if isinstance(dependency, (str, PackageInfo)) + else dependency ) def _convert(d: Requirement) -> Requirement | None: @@ -420,11 +456,13 @@ def _convert(d: Requirement) -> Requirement | None: return res deps = [d for d in (_convert(d) for d in deps) if d is not None] + if status: + removables.append(depinfo) project["dependencies"] = tomlkit.array().multiline(True) project["dependencies"].extend(str(d) for d in deps) - return status + return removables def add_adapter(self, *adapters: PackageInfo) -> None: if not adapters: diff --git a/nb_cli/exceptions.py b/nb_cli/exceptions.py index bf9a434d..def0d23b 100644 --- a/nb_cli/exceptions.py +++ b/nb_cli/exceptions.py @@ -28,3 +28,7 @@ class LocalCacheExpired(RuntimeError): class NoSelectablePackageError(RuntimeError): """Raised when there is no selectable package.""" + + +class ProcessExecutionError(RuntimeError): + """Raised when a subprocess execution fails.""" diff --git a/nb_cli/handlers/__init__.py b/nb_cli/handlers/__init__.py index e638b56b..22af7cc2 100644 --- a/nb_cli/handlers/__init__.py +++ b/nb_cli/handlers/__init__.py @@ -50,6 +50,12 @@ # isort: split +from .environment import EnvironmentExecutor as EnvironmentExecutor +from .environment import all_environment_managers as all_environment_managers +from .environment import probe_environment_manager as probe_environment_manager + +# isort: split + # pip from .pip import call_pip as call_pip from .pip import call_pip_list as call_pip_list diff --git a/nb_cli/handlers/environment.py b/nb_cli/handlers/environment.py new file mode 100644 index 00000000..d764cc12 --- /dev/null +++ b/nb_cli/handlers/environment.py @@ -0,0 +1,502 @@ +import abc +from pathlib import Path +from shutil import which +from asyncio.subprocess import Process +from collections.abc import Mapping, Sequence +from typing import IO, TYPE_CHECKING, Any, Union, Literal, ClassVar, TypeAlias, overload + +import click +from packaging.requirements import Requirement + +from nb_cli import _ +from nb_cli.consts import WINDOWS +from nb_cli.cli.utils import run_sync +from nb_cli.exceptions import ProcessExecutionError +from nb_cli.config import GLOBAL_CONFIG, ConfigManager + +from .process import create_process +from .meta import ( + DEFAULT_PYTHON, + WINDOWS_DEFAULT_PYTHON, + get_project_root, + get_default_python, + requires_project_root, +) + +if TYPE_CHECKING: + import os + +_manager_features: dict[str, str] = { + "uv": "uv.lock", + "pdm": "pdm.lock", + "poetry": "poetry.lock", +} +_manager_exec = [*_manager_features.keys(), "pip"] + +FdFile: TypeAlias = int | IO[bytes] | IO[str] + + +@requires_project_root +@run_sync +def probe_environment_manager(*, cwd: Path | None = None) -> tuple[str, str]: + """Probe the environment manager available and used in the current project. + + Returns: + A tuple of (project_manager_inferred, available_manager). + """ + project_root = get_project_root(cwd) + + current = next( + iter( + m for m, lock in _manager_features.items() if (project_root / lock).exists() + ), + "pip", + ) + + available = next(iter(m for m in [current, "pip"] if which(m) is not None)) + + return current, available + + +@run_sync +def all_environment_managers() -> list[str]: + """Get all available environment managers on the system. + + Returns: + A list of available environment manager names. + """ + return [m for m in _manager_exec if which(m) is not None] + + +class EnvironmentExecutor(metaclass=abc.ABCMeta): + """Abstract base class for environment executors.""" + + _executors: ClassVar[dict[str, type["EnvironmentExecutor"]]] = {} + _executable: ClassVar[str] + cwd: Path + stdin: FdFile | None + stdout: FdFile | None + stderr: FdFile | None + env: Mapping[str, str] | None + executable: str + + @abc.abstractmethod + def __init__( + self, + *, + cwd: Path, + stdin: FdFile | None = None, + stdout: FdFile | None = None, + stderr: FdFile | None = None, + env: Mapping[str, str] | None = None, + executable: str | None = None, + ) -> None: + self.cwd = cwd + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.executable = executable or self._executable + self.env = env + + def __init_subclass__(cls, /, *, manager_name: str, **kwargs) -> None: + cls._executors[manager_name] = cls + cls._executable = which(manager_name) or manager_name + + async def run( + self, *args: Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] + ) -> Process: + """Run subprocess with the given parameters.""" + return await create_process( + self.executable, + *args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + + @overload + @classmethod + def of(cls, name: Literal["uv"]) -> type["UvEnvironmentExecutor"]: ... + @overload + @classmethod + def of(cls, name: Literal["pdm"]) -> type["PdmEnvironmentExecutor"]: ... + @overload + @classmethod + def of(cls, name: Literal["poetry"]) -> type["PoetryEnvironmentExecutor"]: ... + @overload + @classmethod + def of(cls, name: Literal["pip"]) -> type["PipEnvironmentExecutor"]: ... + @overload + @classmethod + def of(cls, name: str) -> type["EnvironmentExecutor"]: ... + + @classmethod + def of(cls, name: str) -> type["EnvironmentExecutor"]: + """Get the executor class for the given environment manager name. + + Args: + name: The name of the environment manager. + Returns: + The executor class corresponding to the given name. + """ + try: + return cls._executors[name] + except KeyError as e: + raise ValueError(f"Unknown environment manager: {name}") from e + + @classmethod + @requires_project_root + async def get( + cls, + name: str | None = None, + *, + toml_manager: ConfigManager = GLOBAL_CONFIG, + cwd: Path | None = None, + stdin: FdFile | None = None, + stdout: FdFile | None = None, + stderr: FdFile | None = None, + env: Mapping[str, str] | None = None, + executable: str | None = None, + ) -> "EnvironmentExecutor": + """Get an instance of the executor for the given environment manager name. + + Args: + name: The name of the environment manager. + cwd: The current working directory for the executor. + stdin: The standard input for the executor. + stdout: The standard output for the executor. + stderr: The standard error for the executor. + env: The environment variables for the executor. + executable: The executable path for the environment manager. + Returns: + An instance of the executor corresponding to the given name. + """ + if name is None: + current, name = await probe_environment_manager(cwd=cwd) + if current != name: + click.secho( + _( + "Warning: The current project uses {current!r} " + "but the available manager is {name!r}." + ).format(current=current, name=name), + fg="yellow", + ) + executor_cls = cls.of(name) + extras: dict[str, Any] + if name == "pip": + if executable is None: + executable = await get_default_python(cwd=cwd) + extras = {"toml_manager": toml_manager} + else: + extras = {} + return executor_cls( + **extras, + cwd=cwd or get_project_root(), + stdin=stdin, + stdout=stdout, + stderr=stderr, + env=env, + executable=executable, + ) + + @abc.abstractmethod + async def init(self, extra_args: Sequence[str] = ()) -> None: + """Initialize the environment.""" + raise NotImplementedError("Init method is not implemented for this manager.") + + @abc.abstractmethod + async def lock(self, extra_args: Sequence[str] = ()) -> None: + """Generate or update the lock file for the environment.""" + raise NotImplementedError("Lock method is not implemented for this manager.") + + @abc.abstractmethod + async def sync(self, extra_args: Sequence[str] = ()) -> None: + """Synchronize the environment with the lock file or configuration.""" + raise NotImplementedError("Sync method is not implemented for this manager.") + + @abc.abstractmethod + async def install( + self, *packages: Requirement, extra_args: Sequence[str] = (), dev: bool = False + ) -> None: + """Install packages into the environment. + + Args: + packages: A list of package specifiers to install. + """ + raise NotImplementedError("Install method is not implemented for this manager.") + + @abc.abstractmethod + async def update( + self, *packages: Requirement, extra_args: Sequence[str] = () + ) -> None: + """Update packages in the environment. + + Args: + packages: A list of package specifiers to update. If None, update all + packages. + """ + raise NotImplementedError("Update method is not implemented for this manager.") + + @abc.abstractmethod + async def uninstall( + self, *packages: Requirement, extra_args: Sequence[str] = () + ) -> None: + """Uninstall packages from the environment. + + Args: + packages: A list of package specifiers to uninstall. + """ + raise NotImplementedError( + "Uninstall method is not implemented for this manager." + ) + + +class UvEnvironmentExecutor(EnvironmentExecutor, manager_name="uv"): + """Environment executor for Uv environment manager.""" + + def __init__( + self, + *, + cwd: Path, + stdin: FdFile | None = None, + stdout: FdFile | None = None, + stderr: FdFile | None = None, + env: Mapping[str, str] | None = None, + executable: str | None = None, + ) -> None: + super().__init__(cwd=cwd, stdin=stdin, stdout=stdout, stderr=stderr, env=env) + + async def init(self, extra_args: Sequence[str] = ()) -> None: + proc = await self.run("init", *extra_args) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to initialize Uv environment.") + + async def lock(self, extra_args: Sequence[str] = ()) -> None: + proc = await self.run("lock", *extra_args) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to lock Uv environment.") + + async def sync(self, extra_args: Sequence[str] = ()) -> None: + proc = await self.run("sync", *extra_args) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to sync Uv environment.") + + async def install( + self, *packages: Requirement, extra_args: Sequence[str] = (), dev: bool = False + ) -> None: + if dev: + extra_args = (*extra_args, "--dev") + proc = await self.run("add", *(str(pkg) for pkg in packages), *extra_args) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to install packages in Uv environment.") + + async def update( + self, *packages: Requirement, extra_args: Sequence[str] = () + ) -> None: + proc = await self.run( + "add", "--upgrade", *(str(pkg) for pkg in packages), *extra_args + ) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to update packages in Uv environment.") + + async def uninstall( + self, *packages: Requirement, extra_args: Sequence[str] = () + ) -> None: + proc = await self.run("remove", *(str(pkg) for pkg in packages), *extra_args) + if await proc.wait() != 0: + raise ProcessExecutionError( + "Failed to uninstall packages from Uv environment." + ) + + +class PdmEnvironmentExecutor(EnvironmentExecutor, manager_name="pdm"): + """Environment executor for PDM environment manager.""" + + def __init__( + self, + *, + cwd: Path, + stdin: FdFile | None = None, + stdout: FdFile | None = None, + stderr: FdFile | None = None, + env: Mapping[str, str] | None = None, + executable: str | None = None, + ) -> None: + super().__init__(cwd=cwd, stdin=stdin, stdout=stdout, stderr=stderr, env=env) + + async def init(self, extra_args: Sequence[str] = ()) -> None: + proc = await self.run("init", *extra_args) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to initialize PDM environment.") + + async def lock(self, extra_args: Sequence[str] = ()) -> None: + proc = await self.run("lock", *extra_args) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to lock PDM environment.") + + async def sync(self, extra_args: Sequence[str] = ()) -> None: + proc = await self.run("sync", *extra_args) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to sync PDM environment.") + + async def install( + self, *packages: Requirement, extra_args: Sequence[str] = (), dev: bool = False + ) -> None: + if dev: + extra_args = (*extra_args, "--dev") + proc = await self.run("add", *(str(pkg) for pkg in packages), *extra_args) + if await proc.wait() != 0: + raise ProcessExecutionError( + "Failed to install packages in PDM environment." + ) + + async def update( + self, *packages: Requirement, extra_args: Sequence[str] = () + ) -> None: + proc = await self.run("update", *(str(pkg) for pkg in packages), *extra_args) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to update packages in PDM environment.") + + async def uninstall( + self, *packages: Requirement, extra_args: Sequence[str | bytes] = () + ) -> None: + proc = await self.run("remove", *(str(pkg) for pkg in packages), *extra_args) + if await proc.wait() != 0: + raise ProcessExecutionError( + "Failed to uninstall packages from PDM environment." + ) + + +class PoetryEnvironmentExecutor(EnvironmentExecutor, manager_name="poetry"): + """Environment executor for Poetry environment manager.""" + + def __init__( + self, + *, + cwd: Path, + stdin: FdFile | None = None, + stdout: FdFile | None = None, + stderr: FdFile | None = None, + env: Mapping[str, str] | None = None, + executable: str | None = None, + ) -> None: + super().__init__(cwd=cwd, stdin=stdin, stdout=stdout, stderr=stderr, env=env) + + async def init(self, extra_args: Sequence[str] = ()) -> None: + proc = await self.run("init", *extra_args) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to initialize Poetry environment.") + + async def lock(self, extra_args: Sequence[str] = ()) -> None: + proc = await self.run("lock", *extra_args) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to lock Poetry environment.") + + async def sync(self, extra_args: Sequence[str] = ()) -> None: + proc = await self.run("install", *extra_args) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to sync Poetry environment.") + + async def install( + self, *packages: Requirement, extra_args: Sequence[str] = (), dev: bool = False + ) -> None: + if dev: + extra_args = (*extra_args, "--dev") + proc = await self.run("add", *(str(pkg) for pkg in packages), *extra_args) + if await proc.wait() != 0: + raise ProcessExecutionError( + "Failed to install packages in Poetry environment." + ) + + async def update( + self, *packages: Requirement, extra_args: Sequence[str] = () + ) -> None: + proc = await self.run("update", *(str(pkg) for pkg in packages), *extra_args) + if await proc.wait() != 0: + raise ProcessExecutionError( + "Failed to update packages in Poetry environment." + ) + + async def uninstall( + self, *packages: Requirement, extra_args: Sequence[str] = () + ) -> None: + proc = await self.run("remove", *(str(pkg) for pkg in packages), *extra_args) + if await proc.wait() != 0: + raise ProcessExecutionError( + "Failed to uninstall packages from Poetry environment." + ) + + +class PipEnvironmentExecutor(EnvironmentExecutor, manager_name="pip"): + """Environment executor for Pip environment manager.""" + + toml_manager: ConfigManager + + def __init__( + self, + toml_manager: ConfigManager = GLOBAL_CONFIG, + *, + cwd: Path, + stdin: FdFile | None = None, + stdout: FdFile | None = None, + stderr: FdFile | None = None, + env: Mapping[str, str] | None = None, + executable: str = WINDOWS_DEFAULT_PYTHON[0] if WINDOWS else DEFAULT_PYTHON[0], + ) -> None: + super().__init__(cwd=cwd, stdin=stdin, stdout=stdout, stderr=stderr, env=env) + self.executable = executable + self.toml_manager = toml_manager + + async def init(self, extra_args: Sequence[str] = ()) -> None: + pass # Pip does not require initialization. + + async def lock(self, extra_args: Sequence[str] = ()) -> None: + pass # Pip does not have a lock mechanism. + + async def sync(self, extra_args: Sequence[str] = ()) -> None: + proc = await self.run("-m", "pip", "install", "-e", ".", *extra_args) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to sync Pip environment.") + + async def install( + self, *packages: Requirement, extra_args: Sequence[str] = (), dev: bool = False + ) -> None: + proc = await self.run( + "-m", "pip", "install", *(str(pkg) for pkg in packages), *extra_args + ) + if await proc.wait() != 0: + raise ProcessExecutionError( + "Failed to install packages in Pip environment." + ) + self.toml_manager.add_dependency(*packages, group="dev" if dev else None) + + async def update( + self, *packages: Requirement, extra_args: Sequence[str] = () + ) -> None: + proc = await self.run( + "-m", + "pip", + "install", + "--upgrade", + *(str(pkg) for pkg in packages), + *extra_args, + ) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to update packages in Pip environment.") + self.toml_manager.update_dependency(*packages) + + async def uninstall( + self, *packages: Requirement, extra_args: Sequence[str] = () + ) -> None: + free_packages = self.toml_manager.remove_dependency(*packages) + if not free_packages: + return + proc = await self.run( + "-m", "pip", "uninstall", *(str(pkg) for pkg in free_packages), *extra_args + ) + if await proc.wait() != 0: + raise ProcessExecutionError( + "Failed to uninstall packages from Pip environment." + ) diff --git a/nb_cli/locale/zh_CN/LC_MESSAGES/nb-cli.po b/nb_cli/locale/zh_CN/LC_MESSAGES/nb-cli.po index bc713c24..f92ac256 100644 --- a/nb_cli/locale/zh_CN/LC_MESSAGES/nb-cli.po +++ b/nb_cli/locale/zh_CN/LC_MESSAGES/nb-cli.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: nb-cli 1.0.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2025-12-10 07:07+0000\n" +"POT-Creation-Date: 2026-01-24 13:20+0000\n" "PO-Revision-Date: 2023-01-11 08:56+0000\n" "Last-Translator: FULL NAME \n" "Language: zh_Hans_CN\n" @@ -34,16 +34,16 @@ msgstr "Python 可执行文件路径." msgid "Auto detect virtual environment." msgstr "自动检测虚拟环境." -#: nb_cli/cli/__init__.py:106 nb_cli/cli/commands/adapter.py:46 -#: nb_cli/cli/commands/cache.py:42 nb_cli/cli/commands/driver.py:42 -#: nb_cli/cli/commands/plugin.py:46 nb_cli/cli/commands/self.py:42 +#: nb_cli/cli/__init__.py:106 nb_cli/cli/commands/adapter.py:45 +#: nb_cli/cli/commands/cache.py:42 nb_cli/cli/commands/driver.py:38 +#: nb_cli/cli/commands/plugin.py:45 nb_cli/cli/commands/self.py:42 #, python-brace-format msgid "Run subcommand {sub_cmd.name!r}" msgstr "运行子命令 {sub_cmd.name!r}" -#: nb_cli/cli/__init__.py:110 nb_cli/cli/commands/adapter.py:53 -#: nb_cli/cli/commands/cache.py:49 nb_cli/cli/commands/driver.py:49 -#: nb_cli/cli/commands/plugin.py:53 nb_cli/cli/commands/self.py:49 +#: nb_cli/cli/__init__.py:110 nb_cli/cli/commands/adapter.py:52 +#: nb_cli/cli/commands/cache.py:49 nb_cli/cli/commands/driver.py:45 +#: nb_cli/cli/commands/plugin.py:52 nb_cli/cli/commands/self.py:49 msgid "Exit NB CLI." msgstr "退出 NB CLI." @@ -51,9 +51,9 @@ msgstr "退出 NB CLI." msgid "Welcome to NoneBot CLI!" msgstr "欢迎使用 NoneBot CLI!" -#: nb_cli/cli/__init__.py:121 nb_cli/cli/commands/adapter.py:59 -#: nb_cli/cli/commands/cache.py:55 nb_cli/cli/commands/driver.py:55 -#: nb_cli/cli/commands/plugin.py:59 nb_cli/cli/commands/self.py:55 +#: nb_cli/cli/__init__.py:121 nb_cli/cli/commands/adapter.py:58 +#: nb_cli/cli/commands/cache.py:55 nb_cli/cli/commands/driver.py:51 +#: nb_cli/cli/commands/plugin.py:58 nb_cli/cli/commands/self.py:55 msgid "What do you want to do?" msgstr "你想要进行什么操作?" @@ -62,77 +62,77 @@ msgstr "你想要进行什么操作?" msgid "Run script {script_name!r}" msgstr "运行脚本 {script_name!r}" -#: nb_cli/cli/utils.py:100 +#: nb_cli/cli/utils.py:105 #, python-brace-format msgid "Package {name} not found." msgstr "包 {name} 未找到." -#: nb_cli/cli/utils.py:102 +#: nb_cli/cli/utils.py:107 msgid "*** You may check with `--include-unpublished` option if supported." msgstr "*** 可以对支持的命令使用 `--include-unpublished` 选项来确认." -#: nb_cli/cli/commands/adapter.py:30 +#: nb_cli/cli/commands/adapter.py:29 msgid "Manage bot adapters." msgstr "管理 bot 适配器." -#: nb_cli/cli/commands/adapter.py:51 nb_cli/cli/commands/cache.py:47 -#: nb_cli/cli/commands/driver.py:47 nb_cli/cli/commands/plugin.py:51 +#: nb_cli/cli/commands/adapter.py:50 nb_cli/cli/commands/cache.py:47 +#: nb_cli/cli/commands/driver.py:43 nb_cli/cli/commands/plugin.py:50 #: nb_cli/cli/commands/self.py:47 msgid "Back to top level." msgstr "返回上一级." -#: nb_cli/cli/commands/adapter.py:71 +#: nb_cli/cli/commands/adapter.py:70 msgid "Open nonebot adapter store." msgstr "打开 NoneBot 适配器商店." -#: nb_cli/cli/commands/adapter.py:78 +#: nb_cli/cli/commands/adapter.py:77 msgid "NB-CLI - NoneBot Adapter Store" msgstr "NB-CLI - NoneBot 适配器商店" -#: nb_cli/cli/commands/adapter.py:83 +#: nb_cli/cli/commands/adapter.py:82 msgid "List nonebot adapters published on nonebot homepage." msgstr "列出 NoneBot 官网上发布的适配器." -#: nb_cli/cli/commands/adapter.py:90 +#: nb_cli/cli/commands/adapter.py:89 msgid "Whether to list installed adapters only in current project." msgstr "是否只列出安装到当前项目的适配器." -#: nb_cli/cli/commands/adapter.py:97 nb_cli/cli/commands/adapter.py:117 -#: nb_cli/cli/commands/adapter.py:145 nb_cli/cli/commands/adapter.py:228 +#: nb_cli/cli/commands/adapter.py:96 nb_cli/cli/commands/adapter.py:116 +#: nb_cli/cli/commands/adapter.py:144 nb_cli/cli/commands/adapter.py:241 msgid "Whether to include unpublished adapters." msgstr "是否要包含已下架的适配器." -#: nb_cli/cli/commands/adapter.py:107 nb_cli/cli/commands/adapter.py:128 +#: nb_cli/cli/commands/adapter.py:106 nb_cli/cli/commands/adapter.py:127 msgid "WARNING: Unpublished adapters may be included." msgstr "警告: 结果中可能含有已下架的适配器." -#: nb_cli/cli/commands/adapter.py:111 +#: nb_cli/cli/commands/adapter.py:110 msgid "Search for nonebot adapters published on nonebot homepage." msgstr "搜索 NoneBot 官网上发布的适配器." -#: nb_cli/cli/commands/adapter.py:123 +#: nb_cli/cli/commands/adapter.py:122 msgid "Adapter name to search:" msgstr "想要搜索的适配器名称:" -#: nb_cli/cli/commands/adapter.py:135 +#: nb_cli/cli/commands/adapter.py:134 msgid "Install nonebot adapter to current project." msgstr "安装适配器到当前项目." -#: nb_cli/cli/commands/adapter.py:163 +#: nb_cli/cli/commands/adapter.py:170 nb_cli/cli/commands/adapter.py:179 msgid "Adapter name to install:" msgstr "想要安装的适配器名称:" -#: nb_cli/cli/commands/adapter.py:174 +#: nb_cli/cli/commands/adapter.py:194 msgid "No available adapter found to install." msgstr "没有可供安装的适配器." -#: nb_cli/cli/commands/adapter.py:180 nb_cli/cli/commands/adapter.py:253 +#: nb_cli/cli/commands/adapter.py:200 nb_cli/cli/commands/adapter.py:266 msgid "" "WARNING: Unpublished adapters may be installed. These adapters may be " "unmaintained or unusable." msgstr "警告: 有可能安装已下架的适配器. 其可能缺少维护或不可用." -#: nb_cli/cli/commands/adapter.py:192 +#: nb_cli/cli/commands/adapter.py:215 #, python-brace-format msgid "" "Errors occurred in installing adapter {adapter.name}\n" @@ -143,87 +143,77 @@ msgstr "" "*** 尝试使用 `--no-restrict-version` 选项执行 `nb adapter install` " "命令在宽松的版本约束下解析依赖可能可以解决此问题." -#: nb_cli/cli/commands/adapter.py:205 +#: nb_cli/cli/commands/adapter.py:227 #, python-brace-format msgid "Failed to add adapter {adapter.name} to config: {e}" msgstr "添加适配器 {adapter.name} 到配置文件失败: {e}" -#: nb_cli/cli/commands/adapter.py:214 -#, python-brace-format -msgid "Failed to add adapter {adapter.name} to dependencies: {e}" -msgstr "添加适配器 {adapter.name} 到依赖项失败: {e}" - -#: nb_cli/cli/commands/adapter.py:221 +#: nb_cli/cli/commands/adapter.py:234 msgid "Update nonebot adapter." msgstr "更新适配器." -#: nb_cli/cli/commands/adapter.py:240 +#: nb_cli/cli/commands/adapter.py:253 msgid "Adapter name to update:" msgstr "想要更新的适配器名称:" -#: nb_cli/cli/commands/adapter.py:247 +#: nb_cli/cli/commands/adapter.py:260 msgid "No installed adapter found to update." msgstr "没有可供更新的适配器:" -#: nb_cli/cli/commands/adapter.py:262 +#: nb_cli/cli/commands/adapter.py:280 #, python-brace-format msgid "Errors occurred in updating adapter {adapter.name}. Aborted." msgstr "更新适配器 {adapter.name} 时发生错误. 已中止." -#: nb_cli/cli/commands/adapter.py:273 -#, python-brace-format -msgid "Failed to update adapter {adapter.name} to dependencies: {e}" -msgstr "更新适配器 {adapter.name} 的依赖项失败: {e}" - -#: nb_cli/cli/commands/adapter.py:282 +#: nb_cli/cli/commands/adapter.py:291 msgid "Uninstall nonebot adapter from current project." msgstr "移除当前项目中的适配器." -#: nb_cli/cli/commands/adapter.py:290 +#: nb_cli/cli/commands/adapter.py:304 msgid "Adapter name to uninstall:" msgstr "想要移除的适配器名称:" -#: nb_cli/cli/commands/adapter.py:297 +#: nb_cli/cli/commands/adapter.py:311 msgid "No installed adapter found to uninstall." msgstr "没有可供移除的适配器." -#: nb_cli/cli/commands/adapter.py:306 +#: nb_cli/cli/commands/adapter.py:318 #, python-brace-format msgid "Failed to remove adapter {adapter.name} from config: {e}" msgstr "从配置文件移除适配器 {adapter.name} 失败: {e}" -#: nb_cli/cli/commands/adapter.py:317 +#: nb_cli/cli/commands/adapter.py:332 msgid "Create a new nonebot adapter." msgstr "新建适配器" -#: nb_cli/cli/commands/adapter.py:325 +#: nb_cli/cli/commands/adapter.py:340 msgid "The adapter template to use." msgstr "使用的适配器模板." -#: nb_cli/cli/commands/adapter.py:336 +#: nb_cli/cli/commands/adapter.py:351 msgid "Adapter name:" msgstr "适配器名称:" -#: nb_cli/cli/commands/adapter.py:357 +#: nb_cli/cli/commands/adapter.py:372 msgid "Where to store the adapter?" msgstr "请输入适配器存储的位置:" -#: nb_cli/cli/commands/adapter.py:358 nb_cli/cli/commands/adapter.py:361 -#: nb_cli/cli/commands/plugin.py:364 nb_cli/cli/commands/plugin.py:367 +#: nb_cli/cli/commands/adapter.py:373 nb_cli/cli/commands/adapter.py:376 +#: nb_cli/cli/commands/plugin.py:374 nb_cli/cli/commands/plugin.py:377 msgid "Other" msgstr "其他" -#: nb_cli/cli/commands/adapter.py:363 nb_cli/cli/commands/adapter.py:372 -#: nb_cli/cli/commands/plugin.py:369 nb_cli/cli/commands/plugin.py:379 +#: nb_cli/cli/commands/adapter.py:378 nb_cli/cli/commands/adapter.py:387 +#: nb_cli/cli/commands/plugin.py:379 nb_cli/cli/commands/plugin.py:389 msgid "Output Dir:" msgstr "输出目录:" -#: nb_cli/cli/commands/adapter.py:369 nb_cli/cli/commands/plugin.py:376 +#: nb_cli/cli/commands/adapter.py:384 nb_cli/cli/commands/plugin.py:386 msgid "Output dir is not a directory!" msgstr "输出目录不是一个文件夹!" -#: nb_cli/cli/commands/adapter.py:374 nb_cli/cli/commands/plugin.py:371 -#: nb_cli/cli/commands/plugin.py:381 +#: nb_cli/cli/commands/adapter.py:389 nb_cli/cli/commands/plugin.py:381 +#: nb_cli/cli/commands/plugin.py:391 msgid "Invalid output dir!" msgstr "无效的输出目录!" @@ -343,48 +333,48 @@ msgstr "成功清除已下架模块缓存." msgid "Successfully cleared all caches." msgstr "成功清除所有缓存." -#: nb_cli/cli/commands/driver.py:26 +#: nb_cli/cli/commands/driver.py:22 msgid "Manage bot driver." msgstr "管理 bot 驱动器." -#: nb_cli/cli/commands/driver.py:67 +#: nb_cli/cli/commands/driver.py:63 msgid "Open nonebot driver store." msgstr "打开 NoneBot 驱动器商店." -#: nb_cli/cli/commands/driver.py:74 +#: nb_cli/cli/commands/driver.py:70 msgid "NB-CLI - NoneBot Driver Store" msgstr "NB-CLI - NoneBot 驱动器商店" -#: nb_cli/cli/commands/driver.py:79 +#: nb_cli/cli/commands/driver.py:75 msgid "List nonebot drivers published on nonebot homepage." msgstr "列出 NoneBot 官网上发布的驱动器." -#: nb_cli/cli/commands/driver.py:86 nb_cli/cli/commands/driver.py:102 -#: nb_cli/cli/commands/driver.py:127 nb_cli/cli/commands/driver.py:184 +#: nb_cli/cli/commands/driver.py:82 nb_cli/cli/commands/driver.py:98 +#: nb_cli/cli/commands/driver.py:123 nb_cli/cli/commands/driver.py:176 msgid "Whether to include unpublished drivers." msgstr "是否要包含已下架的驱动器." -#: nb_cli/cli/commands/driver.py:92 nb_cli/cli/commands/driver.py:113 +#: nb_cli/cli/commands/driver.py:88 nb_cli/cli/commands/driver.py:109 msgid "WARNING: Unpublished drivers may be included." msgstr "警告: 结果中可能含有已下架的驱动器." -#: nb_cli/cli/commands/driver.py:96 +#: nb_cli/cli/commands/driver.py:92 msgid "Search for nonebot drivers published on nonebot homepage." msgstr "搜索 NoneBot 官网上发布的驱动器." -#: nb_cli/cli/commands/driver.py:108 +#: nb_cli/cli/commands/driver.py:104 msgid "Driver name to search:" msgstr "想要搜索的驱动器名称:" -#: nb_cli/cli/commands/driver.py:120 +#: nb_cli/cli/commands/driver.py:116 msgid "Install nonebot driver to current project." msgstr "安装驱动器到当前项目." -#: nb_cli/cli/commands/driver.py:139 +#: nb_cli/cli/commands/driver.py:135 msgid "Driver name to install:" msgstr "想要安装的驱动器名称:" -#: nb_cli/cli/commands/driver.py:149 nb_cli/cli/commands/driver.py:206 +#: nb_cli/cli/commands/driver.py:146 nb_cli/cli/commands/driver.py:198 msgid "" "WARNING: Unpublished drivers may be installed. These drivers may be " "unmaintained or unusable." @@ -395,98 +385,83 @@ msgstr "警告: 有可能安装已下架的驱动器. 其可能缺少维护或 msgid "Errors occurred in installing driver {driver.name}. Aborted." msgstr "安装驱动器 {driver.name} 时发生错误. 已中止." -#: nb_cli/cli/commands/driver.py:170 -#, python-brace-format -msgid "Failed to add driver {driver.name} to dependencies: {e}" -msgstr "添加适配器 {adapter.name} 到依赖项失败: {e}" - -#: nb_cli/cli/commands/driver.py:177 +#: nb_cli/cli/commands/driver.py:169 msgid "Update nonebot driver." msgstr "更新驱动器." -#: nb_cli/cli/commands/driver.py:196 +#: nb_cli/cli/commands/driver.py:188 msgid "Driver name to update:" msgstr "想要更新的驱动器名称:" -#: nb_cli/cli/commands/driver.py:216 +#: nb_cli/cli/commands/driver.py:212 #, python-brace-format msgid "Errors occurred in updating driver {driver.name}. Aborted." msgstr "更新驱动器 {driver.name} 时发生错误. 已中止." -#: nb_cli/cli/commands/driver.py:227 -#, python-brace-format -msgid "Failed to update driver {driver.name} to dependencies: {e}" -msgstr "更新驱动器 {adapter.name} 的依赖项失败: {e}" - -#: nb_cli/cli/commands/driver.py:236 +#: nb_cli/cli/commands/driver.py:223 msgid "Uninstall nonebot driver from current project." msgstr "移除当前项目中的驱动器." -#: nb_cli/cli/commands/driver.py:244 +#: nb_cli/cli/commands/driver.py:231 msgid "Driver name to uninstall:" msgstr "想要移除的驱动器名称:" -#: nb_cli/cli/commands/driver.py:258 -#, python-brace-format -msgid "Failed to remove driver {driver.name} from dependencies: {e}" -msgstr "从依赖项中移除驱动器 {adapter.name} 失败: {e}" - -#: nb_cli/cli/commands/plugin.py:30 +#: nb_cli/cli/commands/plugin.py:29 msgid "Manage bot plugins." msgstr "管理 bot 插件." -#: nb_cli/cli/commands/plugin.py:71 +#: nb_cli/cli/commands/plugin.py:70 msgid "Open nonebot plugin store." msgstr "打开 NoneBot 插件商店." -#: nb_cli/cli/commands/plugin.py:78 +#: nb_cli/cli/commands/plugin.py:77 msgid "NB-CLI - NoneBot Plugin Store" msgstr "NB-CLI - NoneBot 插件商店" -#: nb_cli/cli/commands/plugin.py:83 +#: nb_cli/cli/commands/plugin.py:82 msgid "List nonebot plugins published on nonebot homepage." msgstr "列出 NoneBot 官网上发布的插件." -#: nb_cli/cli/commands/plugin.py:90 +#: nb_cli/cli/commands/plugin.py:89 msgid "Whether to list installed plugins only in current project." msgstr "是否只列出安装到当前项目的插件." -#: nb_cli/cli/commands/plugin.py:97 nb_cli/cli/commands/plugin.py:117 -#: nb_cli/cli/commands/plugin.py:145 nb_cli/cli/commands/plugin.py:228 +#: nb_cli/cli/commands/plugin.py:96 nb_cli/cli/commands/plugin.py:116 +#: nb_cli/cli/commands/plugin.py:144 nb_cli/cli/commands/plugin.py:239 msgid "Whether to include unpublished plugins." msgstr "是否要包含已下架的插件." -#: nb_cli/cli/commands/plugin.py:107 nb_cli/cli/commands/plugin.py:128 +#: nb_cli/cli/commands/plugin.py:106 nb_cli/cli/commands/plugin.py:127 msgid "WARNING: Unpublished plugins may be included." msgstr "警告: 结果中可能含有已下架的插件." -#: nb_cli/cli/commands/plugin.py:111 +#: nb_cli/cli/commands/plugin.py:110 msgid "Search for nonebot plugins published on nonebot homepage." msgstr "搜索 NoneBot 官网上发布的插件." -#: nb_cli/cli/commands/plugin.py:123 +#: nb_cli/cli/commands/plugin.py:122 msgid "Plugin name to search:" msgstr "想要搜索的插件名称:" -#: nb_cli/cli/commands/plugin.py:135 +#: nb_cli/cli/commands/plugin.py:134 msgid "Install nonebot plugin to current project." msgstr "安装插件到当前项目." -#: nb_cli/cli/commands/plugin.py:163 +#: nb_cli/cli/commands/plugin.py:170 nb_cli/cli/commands/plugin.py:179 msgid "Plugin name to install:" msgstr "想要安装的插件名称:" -#: nb_cli/cli/commands/plugin.py:174 +#: nb_cli/cli/commands/plugin.py:192 msgid "No available plugin found to install." msgstr "没有可供安装的插件." -#: nb_cli/cli/commands/plugin.py:180 nb_cli/cli/commands/plugin.py:253 +#: nb_cli/cli/commands/plugin.py:198 nb_cli/cli/commands/plugin.py:264 msgid "" "WARNING: Unpublished plugins may be installed. These plugins may be " "unmaintained or unusable." msgstr "警告: 有可能安装已下架的插件. 其可能缺少维护或不可用." -#: nb_cli/cli/commands/plugin.py:192 +#: nb_cli/cli/commands/plugin.py:213 #, python-brace-format msgid "" "Errors occurred in installing plugin {plugin.name}\n" @@ -497,299 +472,301 @@ msgstr "" "*** 尝试使用 `--no-restrict-version` 选项执行 `nb plugin install` " "命令在宽松的版本约束下解析依赖可能可以解决此问题." -#: nb_cli/cli/commands/plugin.py:205 +#: nb_cli/cli/commands/plugin.py:225 #, python-brace-format msgid "Failed to add plugin {plugin.name} to config: {e}" msgstr "添加插件 {plugin.name} 到配置文件失败: {e}" -#: nb_cli/cli/commands/plugin.py:214 -#, python-brace-format -msgid "Failed to add plugin {plugin.name} to dependencies: {e}" -msgstr "添加插件 {plugin.name} 到依赖项失败: {e}" - -#: nb_cli/cli/commands/plugin.py:221 +#: nb_cli/cli/commands/plugin.py:232 msgid "Update nonebot plugin." msgstr "更新插件." -#: nb_cli/cli/commands/plugin.py:240 +#: nb_cli/cli/commands/plugin.py:251 msgid "Plugin name to update:" msgstr "想要更新的插件名称:" -#: nb_cli/cli/commands/plugin.py:247 +#: nb_cli/cli/commands/plugin.py:258 msgid "No installed plugin found to update." msgstr "没有可供更新的插件:" -#: nb_cli/cli/commands/plugin.py:262 +#: nb_cli/cli/commands/plugin.py:275 #, python-brace-format msgid "Errors occurred in updating plugin {plugin.name}. Aborted." msgstr "更新插件 {plugin.name} 时发生错误. 已中止." -#: nb_cli/cli/commands/plugin.py:273 -#, python-brace-format -msgid "Failed to update plugin {plugin.name} to dependencies: {e}" -msgstr "更新插件 {plugin.name} 的依赖项失败: {e}" - -#: nb_cli/cli/commands/plugin.py:282 +#: nb_cli/cli/commands/plugin.py:286 msgid "Uninstall nonebot plugin from current project." msgstr "移除当前项目中的插件." -#: nb_cli/cli/commands/plugin.py:290 +#: nb_cli/cli/commands/plugin.py:299 msgid "Plugin name to uninstall:" msgstr "想要移除的插件名称:" -#: nb_cli/cli/commands/plugin.py:297 +#: nb_cli/cli/commands/plugin.py:306 msgid "No installed plugin found to uninstall." msgstr "没有可供移除的插件." -#: nb_cli/cli/commands/plugin.py:306 +#: nb_cli/cli/commands/plugin.py:313 #, python-brace-format msgid "Failed to remove plugin {plugin.name} from config: {e}" msgstr "从配置文件中移除插件 {plugin.name} 失败: {e}" -#: nb_cli/cli/commands/plugin.py:317 +#: nb_cli/cli/commands/plugin.py:327 msgid "Create a new nonebot plugin." msgstr "创建一个新的插件." -#: nb_cli/cli/commands/plugin.py:326 +#: nb_cli/cli/commands/plugin.py:336 msgid "The plugin template to use." msgstr "使用的插件模板." -#: nb_cli/cli/commands/plugin.py:338 +#: nb_cli/cli/commands/plugin.py:348 msgid "Plugin name:" msgstr "插件名称:" -#: nb_cli/cli/commands/plugin.py:346 +#: nb_cli/cli/commands/plugin.py:356 msgid "Use nested plugin?" msgstr "使用嵌套插件?" -#: nb_cli/cli/commands/plugin.py:363 nb_cli/cli/commands/project.py:245 +#: nb_cli/cli/commands/plugin.py:373 nb_cli/cli/commands/project.py:247 msgid "Where to store the plugin?" msgstr "请输入插件存储位置:" -#: nb_cli/cli/commands/project.py:54 +#: nb_cli/cli/commands/project.py:56 msgid "bootstrap (for beginner or user)" msgstr "bootstrap (初学者或用户)" -#: nb_cli/cli/commands/project.py:55 +#: nb_cli/cli/commands/project.py:57 msgid "simple (for plugin developer)" msgstr "simple (插件开发者)" -#: nb_cli/cli/commands/project.py:103 +#: nb_cli/cli/commands/project.py:105 msgid "Loading adapters..." msgstr "正在加载适配器..." -#: nb_cli/cli/commands/project.py:105 +#: nb_cli/cli/commands/project.py:107 msgid "Loading drivers..." msgstr "正在加载驱动器..." -#: nb_cli/cli/commands/project.py:110 +#: nb_cli/cli/commands/project.py:112 msgid "Project Name:" msgstr "项目名称:" -#: nb_cli/cli/commands/project.py:112 nb_cli/cli/commands/project.py:120 +#: nb_cli/cli/commands/project.py:114 nb_cli/cli/commands/project.py:122 msgid "Invalid project name!" msgstr "无效的项目名称!" -#: nb_cli/cli/commands/project.py:127 +#: nb_cli/cli/commands/project.py:129 msgid "Current folder is not empty. Overwrite existing files?" msgstr "当前文件夹非空,是否要覆盖现有文件?" -#: nb_cli/cli/commands/project.py:130 +#: nb_cli/cli/commands/project.py:132 msgid "Stopped creating bot." msgstr "停止创建机器人." -#: nb_cli/cli/commands/project.py:139 +#: nb_cli/cli/commands/project.py:141 msgid "Which adapter(s) would you like to use?" msgstr "要使用哪些适配器?" -#: nb_cli/cli/commands/project.py:149 +#: nb_cli/cli/commands/project.py:151 msgid "You haven't chosen any adapter! Please confirm." msgstr "你没有选择任何适配器! 请确认." -#: nb_cli/cli/commands/project.py:163 +#: nb_cli/cli/commands/project.py:165 msgid "Which driver(s) would you like to use?" msgstr "要使用哪些驱动器?" -#: nb_cli/cli/commands/project.py:171 +#: nb_cli/cli/commands/project.py:173 msgid "Chosen drivers is not valid!" msgstr "选择的驱动器不合法!" -#: nb_cli/cli/commands/project.py:185 +#: nb_cli/cli/commands/project.py:187 msgid "User global (default, suitable for single instance in single user)" msgstr "用户全局 (默认, 适用于单用户下单实例)" -#: nb_cli/cli/commands/project.py:186 +#: nb_cli/cli/commands/project.py:188 msgid "Current project (suitable for multiple/portable instances)" msgstr "当前项目 (适用于多实例/便携实例)" -#: nb_cli/cli/commands/project.py:188 +#: nb_cli/cli/commands/project.py:190 msgid "" "User global (isolate by project name, suitable for multiple instances in " "single user)" msgstr "用户全局 (按项目名称隔离, 适用于单用户下多实例)" -#: nb_cli/cli/commands/project.py:191 +#: nb_cli/cli/commands/project.py:193 msgid "Custom storage location (for advanced users)" msgstr "自定义存储位置 (高级用户)" -#: nb_cli/cli/commands/project.py:196 +#: nb_cli/cli/commands/project.py:198 msgid "Which strategy of local storage would you like to use?" msgstr "要使用什么本地存储策略?" -#: nb_cli/cli/commands/project.py:219 +#: nb_cli/cli/commands/project.py:221 msgid "Cache directory to use:" msgstr "要使用的缓存目录:" -#: nb_cli/cli/commands/project.py:224 +#: nb_cli/cli/commands/project.py:226 msgid "Data directory to use:" msgstr "要使用的数据目录:" -#: nb_cli/cli/commands/project.py:229 +#: nb_cli/cli/commands/project.py:231 msgid "Config directory to use:" msgstr "要使用的配置目录:" -#: nb_cli/cli/commands/project.py:241 +#: nb_cli/cli/commands/project.py:243 #, python-brace-format msgid "1) In a \"{dir_name}\" folder" msgstr "1) 在 \"{dir_name}\" 文件夹中" -#: nb_cli/cli/commands/project.py:242 +#: nb_cli/cli/commands/project.py:244 msgid "2) In a \"src\" folder" msgstr "2) 在 \"src\" 文件夹中" -#: nb_cli/cli/commands/project.py:252 +#: nb_cli/cli/commands/project.py:254 msgid "Which developer tool(s) would you like to use?" msgstr "要使用哪些开发工具?" -#: nb_cli/cli/commands/project.py:254 nb_cli/cli/commands/project.py:255 +#: nb_cli/cli/commands/project.py:256 nb_cli/cli/commands/project.py:257 msgid " (Recommended)" msgstr " (推荐)" -#: nb_cli/cli/commands/project.py:257 +#: nb_cli/cli/commands/project.py:259 msgid " (Advanced user)" msgstr " (高级用户)" -#: nb_cli/cli/commands/project.py:262 +#: nb_cli/cli/commands/project.py:264 msgid "Cannot choose 'Pylance/Pyright' and 'BasedPyright' at the same time." msgstr "不能同时选择 'Pylance/Pyright' 和 'BasedPyright'." -#: nb_cli/cli/commands/project.py:279 +#: nb_cli/cli/commands/project.py:281 msgid "Create a NoneBot project." msgstr "创建一个 NoneBot 项目." -#: nb_cli/cli/commands/project.py:287 +#: nb_cli/cli/commands/project.py:289 msgid "The project template to use." msgstr "使用的项目模板." -#: nb_cli/cli/commands/project.py:292 +#: nb_cli/cli/commands/project.py:294 msgid "The python interpreter virtualenv is installed into." msgstr "虚拟环境使用的 Python 解释器." -#: nb_cli/cli/commands/project.py:309 +#: nb_cli/cli/commands/project.py:311 msgid "Select a template to use:" msgstr "选择一个要使用的模板:" -#: nb_cli/cli/commands/project.py:331 +#: nb_cli/cli/commands/project.py:333 msgid "Install dependencies now?" msgstr "立即安装依赖?" -#: nb_cli/cli/commands/project.py:346 +#: nb_cli/cli/commands/project.py:348 +msgid "Which project manager would you like to use?" +msgstr "要使用哪个项目管理器?" + +#: nb_cli/cli/commands/project.py:357 msgid "Create virtual environment?" msgstr "创建虚拟环境?" -#: nb_cli/cli/commands/project.py:353 +#: nb_cli/cli/commands/project.py:366 #, python-brace-format msgid "Creating virtual environment in {venv_dir} ..." msgstr "在 {venv_dir} 中创建虚拟环境..." -#: nb_cli/cli/commands/project.py:379 +#: nb_cli/cli/commands/project.py:391 +msgid "" +"Failed to install dependencies! You should install the dependencies " +"manually." +msgstr "安装依赖失败! 请手动安装依赖." + +#: nb_cli/cli/commands/project.py:401 +msgid "Install developer dependencies?" +msgstr "安装开发依赖?" + +#: nb_cli/cli/commands/project.py:413 +msgid "Failed to install developer dependencies! You may install them manually." +msgstr "安装开发依赖失败! 可以手动安装依赖." + +#: nb_cli/cli/commands/project.py:426 msgid "Which builtin plugin(s) would you like to use?" msgstr "要使用哪些内置插件?" -#: nb_cli/cli/commands/project.py:392 +#: nb_cli/cli/commands/project.py:436 #, python-brace-format msgid "Failed to add builtin plugins {builtin_plugins} to config: {e}" msgstr "添加内置插件 {builtin_plugins} 到配置文件失败: {e}" -#: nb_cli/cli/commands/project.py:403 +#: nb_cli/cli/commands/project.py:448 msgid "Which official plugins would you like to use?" msgstr "要使用哪些官方插件?" -#: nb_cli/cli/commands/project.py:423 -msgid "Failed to install plugins! You should install the plugins manually." -msgstr "安装插件失败! 请手动安装插件." +#: nb_cli/cli/commands/project.py:465 +msgid "Failed to install plugins! You may install the plugins manually." +msgstr "安装插件失败! 可以手动安装插件." -#: nb_cli/cli/commands/project.py:433 -msgid "" -"Failed to install dependencies! You should install the dependencies " -"manually." -msgstr "安装依赖失败! 请手动安装依赖." - -#: nb_cli/cli/commands/project.py:439 +#: nb_cli/cli/commands/project.py:473 msgid "Done!" msgstr "完成!" -#: nb_cli/cli/commands/project.py:440 +#: nb_cli/cli/commands/project.py:474 msgid "Run the following command to start your bot:" msgstr "运行以下命令来启动你的机器人:" -#: nb_cli/cli/commands/project.py:447 +#: nb_cli/cli/commands/project.py:481 msgid "Generate entry file of your bot." msgstr "生成机器人的入口文件." -#: nb_cli/cli/commands/project.py:453 +#: nb_cli/cli/commands/project.py:487 msgid "The file script saved to." msgstr "脚本文件保存路径." -#: nb_cli/cli/commands/project.py:462 +#: nb_cli/cli/commands/project.py:496 msgid "Run the bot in current folder." msgstr "在当前文件夹中运行机器人." -#: nb_cli/cli/commands/project.py:469 +#: nb_cli/cli/commands/project.py:503 msgid "Exist entry file of your bot." msgstr "存在的机器人入口文件." -#: nb_cli/cli/commands/project.py:476 +#: nb_cli/cli/commands/project.py:510 msgid "Reload the bot when file changed." msgstr "当文件发生变化时重新加载机器人." -#: nb_cli/cli/commands/project.py:482 +#: nb_cli/cli/commands/project.py:516 msgid "Paths to watch for changes." msgstr "要监视变化的路径." -#: nb_cli/cli/commands/project.py:488 +#: nb_cli/cli/commands/project.py:522 msgid "Files to watch for changes." msgstr "要监视变化的文件." -#: nb_cli/cli/commands/project.py:494 +#: nb_cli/cli/commands/project.py:528 msgid "Files to ignore for changes." msgstr "要忽略变化的文件." -#: nb_cli/cli/commands/project.py:501 +#: nb_cli/cli/commands/project.py:535 msgid "Delay time for reloading in seconds." msgstr "重新加载的延迟时间(秒)." -#: nb_cli/cli/commands/project.py:534 +#: nb_cli/cli/commands/project.py:568 msgid "Upgrade the project format of your bot." msgstr "升级机器人的项目格式." -#: nb_cli/cli/commands/project.py:539 +#: nb_cli/cli/commands/project.py:573 msgid "Are you sure to upgrade the project format?" msgstr "你确定要升级项目格式吗?" -#: nb_cli/cli/commands/project.py:542 +#: nb_cli/cli/commands/project.py:576 msgid "Successfully upgraded project format." msgstr "成功升级项目格式." -#: nb_cli/cli/commands/project.py:546 +#: nb_cli/cli/commands/project.py:580 msgid "Downgrade the project format of your bot." msgstr "降级机器人的项目格式." -#: nb_cli/cli/commands/project.py:551 +#: nb_cli/cli/commands/project.py:585 msgid "Are you sure to downgrade the project format?" msgstr "你确定要降级项目格式吗?" -#: nb_cli/cli/commands/project.py:554 +#: nb_cli/cli/commands/project.py:588 msgid "Successfully downgraded project format." msgstr "成功降级项目格式." @@ -821,21 +798,21 @@ msgstr "要卸载的包名?" msgid "List installed packages in cli venv." msgstr "列出 cli 虚拟环境中已安装的包." -#: nb_cli/config/parser.py:139 +#: nb_cli/config/parser.py:169 msgid "Invalid project config format." msgstr "无效的项目配置格式." -#: nb_cli/config/parser.py:149 +#: nb_cli/config/parser.py:179 #, python-brace-format msgid "Cannot find project root directory! {config_file} file not exists." msgstr "无法找到项目根目录! {config_file} 文件不存在." -#: nb_cli/config/parser.py:190 +#: nb_cli/config/parser.py:220 #, python-brace-format msgid "Using python: {python_path}" msgstr "使用 Python: {python_path}" -#: nb_cli/config/parser.py:454 +#: nb_cli/config/parser.py:616 msgid "" "WARNING: Legacy configuration format detected.\n" "*** Use `nb upgrade-format` to upgrade to the new format." @@ -843,6 +820,13 @@ msgstr "" "警告: 检测到旧的项目格式.\n" "*** 使用 `nb upgrade-format` 升级至新格式." +#: nb_cli/handlers/environment.py:181 +#, python-brace-format +msgid "" +"Warning: The current project uses {current!r} but the available manager " +"is {name!r}." +msgstr "警告: 当前项目使用 {current!r} 但可用管理器为 {name!r}." + #: nb_cli/handlers/meta.py:93 msgid "Cannot find a valid Python interpreter." msgstr "无法找到可用的 Python 解释器." diff --git a/pdm.lock b/pdm.lock index 86ff572c..9ef4619c 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev", "docs", "i18n"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:f614293055542f68a99d8613388f428e0eecfb3ca4e9d12158ca00d7e4e3b3ae" +content_hash = "sha256:04c4a8ddd64dfe2fbfe37f8e6b8986ca103120a6704fc72f42ec1fecd8b4662b" [[metadata.targets]] requires_python = "~=3.10" @@ -691,6 +691,17 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] +[[package]] +name = "pip" +version = "25.3" +requires_python = ">=3.9" +summary = "The PyPA recommended tool for installing Python packages." +groups = ["default"] +files = [ + {file = "pip-25.3-py3-none-any.whl", hash = "sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd"}, + {file = "pip-25.3.tar.gz", hash = "sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343"}, +] + [[package]] name = "platformdirs" version = "4.5.1" diff --git a/pyproject.toml b/pyproject.toml index 18809a5b..f340f9bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dependencies = [ "rich>=14.1.0", "textual>=5.3.0", "packaging>=25.0", + "pip>=25.3", ] [project.urls]