From 2a4feaf3ce0b72c3efb4383a9926226b2af297a9 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Tue, 23 Dec 2025 16:45:42 +0000 Subject: [PATCH 01/15] :recycle: add `Requirement` support for raw dependency management --- nb_cli/config/parser.py | 61 +++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/nb_cli/config/parser.py b/nb_cli/config/parser.py index 3b6d1c0a..4e74cf7a 100644 --- a/nb_cli/config/parser.py +++ b/nb_cli/config/parser.py @@ -329,17 +329,21 @@ def get_dependencies(self) -> list[Requirement]: deps: list[str] = data.setdefault("project", {}).setdefault("dependencies", []) return [Requirement(d) for d in deps] - def add_dependency(self, *dependencies: str | PackageInfo) -> None: + def add_dependency(self, *dependencies: str | 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 - 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 +358,32 @@ 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) - 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 +402,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 +439,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: From 21e128c1806222be69d3389bfcbd673b17d05170 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Tue, 23 Dec 2025 17:18:58 +0000 Subject: [PATCH 02/15] :sparkles: add basic environment manager support --- nb_cli/exceptions.py | 4 + nb_cli/handlers/environment.py | 593 +++++++++++++++++++++++++++++++++ 2 files changed, 597 insertions(+) create mode 100644 nb_cli/handlers/environment.py 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/environment.py b/nb_cli/handlers/environment.py new file mode 100644 index 00000000..16427352 --- /dev/null +++ b/nb_cli/handlers/environment.py @@ -0,0 +1,593 @@ +import abc +from pathlib import Path +from shutil import which +from collections.abc import Mapping, Sequence +from typing import IO, Literal, ClassVar, TypeAlias, overload + +from packaging.requirements import Requirement + +from nb_cli.cli.utils import run_sync +from nb_cli.config import ConfigManager +from nb_cli.exceptions import ProcessExecutionError + +from .process import create_process +from .meta import get_project_root, requires_project_root + +_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, *_manager_exec] if which(m) is not None)) + + return current, available + + +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 + + @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, + ) -> None: + self.cwd = cwd + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.env = env + + def __init_subclass__(cls, /, *, name: str, **kwargs) -> None: + cls._executors[name] = cls + cls.executable = which(name) or name + + @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 + + @abc.abstractmethod + async def init(self, extra_args: Sequence[str | bytes] = ()) -> None: + """Initialize the environment.""" + raise NotImplementedError("Init method is not implemented for this manager.") + + @abc.abstractmethod + async def lock(self, extra_args: Sequence[str | bytes] = ()) -> 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 | bytes] = ()) -> 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 | bytes] = () + ) -> 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 | bytes] = () + ) -> 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 | bytes] = () + ) -> 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, 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, + ) -> None: + super().__init__(cwd=cwd, stdin=stdin, stdout=stdout, stderr=stderr, env=env) + + async def init(self, extra_args: Sequence[str | bytes] = ()) -> None: + proc = await create_process( + "uv", + "init", + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to initialize Uv environment.") + + async def lock(self, extra_args: Sequence[str | bytes] = ()) -> None: + proc = await create_process( + "uv", + "lock", + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to lock Uv environment.") + + async def sync(self, extra_args: Sequence[str | bytes] = ()) -> None: + proc = await create_process( + "uv", + "sync", + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to sync Uv environment.") + + async def install( + self, *packages: Requirement, extra_args: Sequence[str | bytes] = () + ) -> None: + proc = await create_process( + "uv", + "add", + *(str(pkg) for pkg in packages), + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to install packages in Uv environment.") + + async def update( + self, *packages: Requirement, extra_args: Sequence[str | bytes] = () + ) -> None: + proc = await create_process( + "uv", + "add", + *(str(pkg) for pkg in packages), + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to update packages in Uv environment.") + + async def uninstall( + self, *packages: Requirement, extra_args: Sequence[str | bytes] = () + ) -> None: + proc = await create_process( + "uv", + "remove", + *(str(pkg) for pkg in packages), + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError( + "Failed to uninstall packages from Uv environment." + ) + + +class PdmEnvironmentExecutor(EnvironmentExecutor, 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, + ) -> None: + super().__init__(cwd=cwd, stdin=stdin, stdout=stdout, stderr=stderr, env=env) + + async def init(self, extra_args: Sequence[str | bytes] = ()) -> None: + proc = await create_process( + "pdm", + "init", + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to initialize PDM environment.") + + async def lock(self, extra_args: Sequence[str | bytes] = ()) -> None: + proc = await create_process( + "pdm", + "lock", + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to lock PDM environment.") + + async def sync(self, extra_args: Sequence[str | bytes] = ()) -> None: + proc = await create_process( + "pdm", + "sync", + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to sync PDM environment.") + + async def install( + self, *packages: Requirement, extra_args: Sequence[str | bytes] = () + ) -> None: + proc = await create_process( + "pdm", + "add", + *(str(pkg) for pkg in packages), + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError( + "Failed to install packages in PDM environment." + ) + + async def update( + self, *packages: Requirement, extra_args: Sequence[str | bytes] = () + ) -> None: + proc = await create_process( + "pdm", + "update", + *(str(pkg) for pkg in packages), + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + 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 create_process( + "pdm", + "remove", + *(str(pkg) for pkg in packages), + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError( + "Failed to uninstall packages from PDM environment." + ) + + +class PoetryEnvironmentExecutor(EnvironmentExecutor, 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, + ) -> None: + super().__init__(cwd=cwd, stdin=stdin, stdout=stdout, stderr=stderr, env=env) + + async def init(self, extra_args: Sequence[str | bytes] = ()) -> None: + proc = await create_process( + "poetry", + "init", + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to initialize Poetry environment.") + + async def lock(self, extra_args: Sequence[str | bytes] = ()) -> None: + proc = await create_process( + "poetry", + "lock", + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to lock Poetry environment.") + + async def sync(self, extra_args: Sequence[str | bytes] = ()) -> None: + proc = await create_process( + "poetry", + "install", + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to sync Poetry environment.") + + async def install( + self, *packages: Requirement, extra_args: Sequence[str | bytes] = () + ) -> None: + proc = await create_process( + "poetry", + "add", + *(str(pkg) for pkg in packages), + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError( + "Failed to install packages in Poetry environment." + ) + + async def update( + self, *packages: Requirement, extra_args: Sequence[str | bytes] = () + ) -> None: + proc = await create_process( + "poetry", + "update", + *(str(pkg) for pkg in packages), + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError( + "Failed to update packages in Poetry environment." + ) + + async def uninstall( + self, *packages: Requirement, extra_args: Sequence[str | bytes] = () + ) -> None: + proc = await create_process( + "poetry", + "remove", + *(str(pkg) for pkg in packages), + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError( + "Failed to uninstall packages from Poetry environment." + ) + + +class PipEnvironmentExecutor(EnvironmentExecutor, name="pip"): + """Environment executor for Pip environment manager.""" + + toml_manager: ConfigManager + + def __init__( + self, + toml_manager: ConfigManager, + *, + cwd: Path, + stdin: FdFile | None = None, + stdout: FdFile | None = None, + stderr: FdFile | None = None, + env: Mapping[str, str] | None = None, + ) -> None: + super().__init__(cwd=cwd, stdin=stdin, stdout=stdout, stderr=stderr, env=env) + self.toml_manager = toml_manager + + async def init(self, extra_args: Sequence[str | bytes] = ()) -> None: + pass # Pip does not require initialization. + + async def lock(self, extra_args: Sequence[str | bytes] = ()) -> None: + pass # Pip does not have a lock mechanism. + + async def sync(self, extra_args: Sequence[str | bytes] = ()) -> None: + proc = await create_process( + "pip", + "install", + ".", + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError("Failed to sync Pip environment.") + + async def install( + self, *packages: Requirement, extra_args: Sequence[str | bytes] = () + ) -> None: + proc = await create_process( + "pip", + "install", + *(str(pkg) for pkg in packages), + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError( + "Failed to install packages in Pip environment." + ) + self.toml_manager.add_dependency(*packages) + + async def update( + self, *packages: Requirement, extra_args: Sequence[str | bytes] = () + ) -> None: + proc = await create_process( + "pip", + "install", + "--upgrade", + *(str(pkg) for pkg in packages), + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + 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 | bytes] = () + ) -> None: + free_packages = self.toml_manager.remove_dependency(*packages) + proc = await create_process( + "pip", + "uninstall", + *(str(pkg) for pkg in free_packages), + *extra_args, + cwd=self.cwd, + stdin=self.stdin, + stdout=self.stdout, + stderr=self.stderr, + env=self.env, + ) + if await proc.wait() != 0: + raise ProcessExecutionError( + "Failed to uninstall packages from Pip environment." + ) From 87b6b9c327d74ae86add4451bee233b3c27ef09c Mon Sep 17 00:00:00 2001 From: worldmozara Date: Fri, 23 Jan 2026 08:07:24 +0000 Subject: [PATCH 03/15] :recycle: refactor environment manager methods to use a common run function --- nb_cli/handlers/environment.py | 289 +++++++-------------------------- 1 file changed, 62 insertions(+), 227 deletions(-) diff --git a/nb_cli/handlers/environment.py b/nb_cli/handlers/environment.py index 16427352..200821f3 100644 --- a/nb_cli/handlers/environment.py +++ b/nb_cli/handlers/environment.py @@ -1,18 +1,22 @@ 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, Literal, ClassVar, TypeAlias, overload +from typing import IO, TYPE_CHECKING, Union, Literal, ClassVar, TypeAlias, overload from packaging.requirements import Requirement from nb_cli.cli.utils import run_sync -from nb_cli.config import ConfigManager from nb_cli.exceptions import ProcessExecutionError +from nb_cli.config import GLOBAL_CONFIG, ConfigManager from .process import create_process from .meta import get_project_root, requires_project_root +if TYPE_CHECKING: + import os + _manager_features: dict[str, str] = { "uv": "uv.lock", "pdm": "pdm.lock", @@ -45,6 +49,16 @@ def probe_environment_manager(*, cwd: Path | None = None) -> tuple[str, str]: 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.""" @@ -76,6 +90,19 @@ def __init_subclass__(cls, /, *, name: str, **kwargs) -> None: cls._executors[name] = cls cls.executable = which(name) or 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( + *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"]: ... @@ -173,94 +200,39 @@ def __init__( super().__init__(cwd=cwd, stdin=stdin, stdout=stdout, stderr=stderr, env=env) async def init(self, extra_args: Sequence[str | bytes] = ()) -> None: - proc = await create_process( - "uv", - "init", - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, - ) + proc = await self.run("uv", "init", *extra_args) if await proc.wait() != 0: raise ProcessExecutionError("Failed to initialize Uv environment.") async def lock(self, extra_args: Sequence[str | bytes] = ()) -> None: - proc = await create_process( - "uv", - "lock", - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, - ) + proc = await self.run("uv", "lock", *extra_args) if await proc.wait() != 0: raise ProcessExecutionError("Failed to lock Uv environment.") async def sync(self, extra_args: Sequence[str | bytes] = ()) -> None: - proc = await create_process( - "uv", - "sync", - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, - ) + proc = await self.run("uv", "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 | bytes] = () ) -> None: - proc = await create_process( - "uv", - "add", - *(str(pkg) for pkg in packages), - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, - ) + proc = await self.run("uv", "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 | bytes] = () ) -> None: - proc = await create_process( - "uv", - "add", - *(str(pkg) for pkg in packages), - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, - ) + proc = await self.run("uv", "add", *(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 | bytes] = () ) -> None: - proc = await create_process( - "uv", - "remove", - *(str(pkg) for pkg in packages), - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, + proc = await self.run( + "uv", "remove", *(str(pkg) for pkg in packages), *extra_args ) if await proc.wait() != 0: raise ProcessExecutionError( @@ -283,60 +255,25 @@ def __init__( super().__init__(cwd=cwd, stdin=stdin, stdout=stdout, stderr=stderr, env=env) async def init(self, extra_args: Sequence[str | bytes] = ()) -> None: - proc = await create_process( - "pdm", - "init", - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, - ) + proc = await self.run("pdm", "init", *extra_args) if await proc.wait() != 0: raise ProcessExecutionError("Failed to initialize PDM environment.") async def lock(self, extra_args: Sequence[str | bytes] = ()) -> None: - proc = await create_process( - "pdm", - "lock", - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, - ) + proc = await self.run("pdm", "lock", *extra_args) if await proc.wait() != 0: raise ProcessExecutionError("Failed to lock PDM environment.") async def sync(self, extra_args: Sequence[str | bytes] = ()) -> None: - proc = await create_process( - "pdm", - "sync", - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, - ) + proc = await self.run("pdm", "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 | bytes] = () ) -> None: - proc = await create_process( - "pdm", - "add", - *(str(pkg) for pkg in packages), - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, + proc = await self.run( + "pdm", "add", *(str(pkg) for pkg in packages), *extra_args ) if await proc.wait() != 0: raise ProcessExecutionError( @@ -346,16 +283,8 @@ async def install( async def update( self, *packages: Requirement, extra_args: Sequence[str | bytes] = () ) -> None: - proc = await create_process( - "pdm", - "update", - *(str(pkg) for pkg in packages), - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, + proc = await self.run( + "pdm", "update", *(str(pkg) for pkg in packages), *extra_args ) if await proc.wait() != 0: raise ProcessExecutionError("Failed to update packages in PDM environment.") @@ -363,16 +292,8 @@ async def update( async def uninstall( self, *packages: Requirement, extra_args: Sequence[str | bytes] = () ) -> None: - proc = await create_process( - "pdm", - "remove", - *(str(pkg) for pkg in packages), - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, + proc = await self.run( + "pdm", "remove", *(str(pkg) for pkg in packages), *extra_args ) if await proc.wait() != 0: raise ProcessExecutionError( @@ -395,60 +316,25 @@ def __init__( super().__init__(cwd=cwd, stdin=stdin, stdout=stdout, stderr=stderr, env=env) async def init(self, extra_args: Sequence[str | bytes] = ()) -> None: - proc = await create_process( - "poetry", - "init", - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, - ) + proc = await self.run("poetry", "init", *extra_args) if await proc.wait() != 0: raise ProcessExecutionError("Failed to initialize Poetry environment.") async def lock(self, extra_args: Sequence[str | bytes] = ()) -> None: - proc = await create_process( - "poetry", - "lock", - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, - ) + proc = await self.run("poetry", "lock", *extra_args) if await proc.wait() != 0: raise ProcessExecutionError("Failed to lock Poetry environment.") async def sync(self, extra_args: Sequence[str | bytes] = ()) -> None: - proc = await create_process( - "poetry", - "install", - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, - ) + proc = await self.run("poetry", "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 | bytes] = () ) -> None: - proc = await create_process( - "poetry", - "add", - *(str(pkg) for pkg in packages), - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, + proc = await self.run( + "poetry", "add", *(str(pkg) for pkg in packages), *extra_args ) if await proc.wait() != 0: raise ProcessExecutionError( @@ -458,16 +344,8 @@ async def install( async def update( self, *packages: Requirement, extra_args: Sequence[str | bytes] = () ) -> None: - proc = await create_process( - "poetry", - "update", - *(str(pkg) for pkg in packages), - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, + proc = await self.run( + "poetry", "update", *(str(pkg) for pkg in packages), *extra_args ) if await proc.wait() != 0: raise ProcessExecutionError( @@ -477,16 +355,8 @@ async def update( async def uninstall( self, *packages: Requirement, extra_args: Sequence[str | bytes] = () ) -> None: - proc = await create_process( - "poetry", - "remove", - *(str(pkg) for pkg in packages), - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, + proc = await self.run( + "poetry", "remove", *(str(pkg) for pkg in packages), *extra_args ) if await proc.wait() != 0: raise ProcessExecutionError( @@ -501,7 +371,7 @@ class PipEnvironmentExecutor(EnvironmentExecutor, name="pip"): def __init__( self, - toml_manager: ConfigManager, + toml_manager: ConfigManager = GLOBAL_CONFIG, *, cwd: Path, stdin: FdFile | None = None, @@ -519,33 +389,15 @@ async def lock(self, extra_args: Sequence[str | bytes] = ()) -> None: pass # Pip does not have a lock mechanism. async def sync(self, extra_args: Sequence[str | bytes] = ()) -> None: - proc = await create_process( - "pip", - "install", - ".", - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, - ) + proc = await self.run("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 | bytes] = () ) -> None: - proc = await create_process( - "pip", - "install", - *(str(pkg) for pkg in packages), - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, + proc = await self.run( + "pip", "install", *(str(pkg) for pkg in packages), *extra_args ) if await proc.wait() != 0: raise ProcessExecutionError( @@ -556,17 +408,8 @@ async def install( async def update( self, *packages: Requirement, extra_args: Sequence[str | bytes] = () ) -> None: - proc = await create_process( - "pip", - "install", - "--upgrade", - *(str(pkg) for pkg in packages), - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, + proc = await self.run( + "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.") @@ -576,16 +419,8 @@ async def uninstall( self, *packages: Requirement, extra_args: Sequence[str | bytes] = () ) -> None: free_packages = self.toml_manager.remove_dependency(*packages) - proc = await create_process( - "pip", - "uninstall", - *(str(pkg) for pkg in free_packages), - *extra_args, - cwd=self.cwd, - stdin=self.stdin, - stdout=self.stdout, - stderr=self.stderr, - env=self.env, + proc = await self.run( + "pip", "uninstall", *(str(pkg) for pkg in free_packages), *extra_args ) if await proc.wait() != 0: raise ProcessExecutionError( From bf21ba29cdbc021a981236bd5332e895694651a8 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Fri, 23 Jan 2026 08:11:27 +0000 Subject: [PATCH 04/15] :recycle: refactor probe_environment_manager to limit available managers to current and pip --- nb_cli/handlers/environment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nb_cli/handlers/environment.py b/nb_cli/handlers/environment.py index 200821f3..55e50ba2 100644 --- a/nb_cli/handlers/environment.py +++ b/nb_cli/handlers/environment.py @@ -44,7 +44,7 @@ def probe_environment_manager(*, cwd: Path | None = None) -> tuple[str, str]: "pip", ) - available = next(iter(m for m in [current, *_manager_exec] if which(m) is not None)) + available = next(iter(m for m in [current, "pip"] if which(m) is not None)) return current, available From 5800c6ffabdbdec8f43bbe0698afe71263e685d6 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Fri, 23 Jan 2026 10:07:03 +0000 Subject: [PATCH 05/15] :recycle: refactor environment handler imports and enhance EnvironmentExecutor with new methods --- nb_cli/handlers/__init__.py | 6 ++ nb_cli/handlers/environment.py | 142 +++++++++++++++++++++++---------- 2 files changed, 108 insertions(+), 40 deletions(-) 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 index 55e50ba2..b7c7cfe5 100644 --- a/nb_cli/handlers/environment.py +++ b/nb_cli/handlers/environment.py @@ -3,16 +3,25 @@ from shutil import which from asyncio.subprocess import Process from collections.abc import Mapping, Sequence -from typing import IO, TYPE_CHECKING, Union, Literal, ClassVar, TypeAlias, overload +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 get_project_root, requires_project_root +from .meta import ( + DEFAULT_PYTHON, + WINDOWS_DEFAULT_PYTHON, + get_project_root, + get_default_python, + requires_project_root, +) if TYPE_CHECKING: import os @@ -63,12 +72,13 @@ class EnvironmentExecutor(metaclass=abc.ABCMeta): """Abstract base class for environment executors.""" _executors: ClassVar[dict[str, type["EnvironmentExecutor"]]] = {} - executable: ClassVar[str] + _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__( @@ -79,22 +89,25 @@ def __init__( 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, /, *, name: str, **kwargs) -> None: cls._executors[name] = cls - cls.executable = which(name) or name + cls._executable = which(name) or 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, @@ -133,6 +146,57 @@ def of(cls, name: str) -> type["EnvironmentExecutor"]: 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. + **kwargs: Additional keyword arguments to pass to the executor constructor. + 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}." + ).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 | bytes] = ()) -> None: """Initialize the environment.""" @@ -196,44 +260,43 @@ def __init__( 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 | bytes] = ()) -> None: - proc = await self.run("uv", "init", *extra_args) + 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 | bytes] = ()) -> None: - proc = await self.run("uv", "lock", *extra_args) + 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 | bytes] = ()) -> None: - proc = await self.run("uv", "sync", *extra_args) + 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 | bytes] = () ) -> None: - proc = await self.run("uv", "add", *(str(pkg) for pkg in packages), *extra_args) + 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 | bytes] = () ) -> None: - proc = await self.run("uv", "add", *(str(pkg) for pkg in packages), *extra_args) + proc = await self.run("add", *(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 | bytes] = () ) -> None: - proc = await self.run( - "uv", "remove", *(str(pkg) for pkg in packages), *extra_args - ) + 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." @@ -251,30 +314,29 @@ def __init__( 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 | bytes] = ()) -> None: - proc = await self.run("pdm", "init", *extra_args) + 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 | bytes] = ()) -> None: - proc = await self.run("pdm", "lock", *extra_args) + 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 | bytes] = ()) -> None: - proc = await self.run("pdm", "sync", *extra_args) + 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 | bytes] = () ) -> None: - proc = await self.run( - "pdm", "add", *(str(pkg) for pkg in packages), *extra_args - ) + 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." @@ -283,18 +345,14 @@ async def install( async def update( self, *packages: Requirement, extra_args: Sequence[str | bytes] = () ) -> None: - proc = await self.run( - "pdm", "update", *(str(pkg) for pkg in packages), *extra_args - ) + 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( - "pdm", "remove", *(str(pkg) for pkg in packages), *extra_args - ) + 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." @@ -312,30 +370,29 @@ def __init__( 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 | bytes] = ()) -> None: - proc = await self.run("poetry", "init", *extra_args) + 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 | bytes] = ()) -> None: - proc = await self.run("poetry", "lock", *extra_args) + 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 | bytes] = ()) -> None: - proc = await self.run("poetry", "install", *extra_args) + 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 | bytes] = () ) -> None: - proc = await self.run( - "poetry", "add", *(str(pkg) for pkg in packages), *extra_args - ) + 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." @@ -344,9 +401,7 @@ async def install( async def update( self, *packages: Requirement, extra_args: Sequence[str | bytes] = () ) -> None: - proc = await self.run( - "poetry", "update", *(str(pkg) for pkg in packages), *extra_args - ) + 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." @@ -355,9 +410,7 @@ async def update( async def uninstall( self, *packages: Requirement, extra_args: Sequence[str | bytes] = () ) -> None: - proc = await self.run( - "poetry", "remove", *(str(pkg) for pkg in packages), *extra_args - ) + 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." @@ -378,8 +431,10 @@ def __init__( 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 | bytes] = ()) -> None: @@ -389,7 +444,7 @@ async def lock(self, extra_args: Sequence[str | bytes] = ()) -> None: pass # Pip does not have a lock mechanism. async def sync(self, extra_args: Sequence[str | bytes] = ()) -> None: - proc = await self.run("pip", "install", "-e", ".", *extra_args) + proc = await self.run("-m", "pip", "install", "-e", ".", *extra_args) if await proc.wait() != 0: raise ProcessExecutionError("Failed to sync Pip environment.") @@ -397,7 +452,7 @@ async def install( self, *packages: Requirement, extra_args: Sequence[str | bytes] = () ) -> None: proc = await self.run( - "pip", "install", *(str(pkg) for pkg in packages), *extra_args + "-m", "pip", "install", *(str(pkg) for pkg in packages), *extra_args ) if await proc.wait() != 0: raise ProcessExecutionError( @@ -409,7 +464,12 @@ async def update( self, *packages: Requirement, extra_args: Sequence[str | bytes] = () ) -> None: proc = await self.run( - "pip", "install", "--upgrade", *(str(pkg) for pkg in packages), *extra_args + "-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.") @@ -419,8 +479,10 @@ async def uninstall( self, *packages: Requirement, extra_args: Sequence[str | bytes] = () ) -> None: free_packages = self.toml_manager.remove_dependency(*packages) + if not free_packages: + return proc = await self.run( - "pip", "uninstall", *(str(pkg) for pkg in free_packages), *extra_args + "-m", "pip", "uninstall", *(str(pkg) for pkg in free_packages), *extra_args ) if await proc.wait() != 0: raise ProcessExecutionError( From 39fd75e32d13bf6200403156b4ebc00710f385a6 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Fri, 23 Jan 2026 10:08:10 +0000 Subject: [PATCH 06/15] :sparkles: add as_requirement method to PackageInfo for enhanced dependency management --- nb_cli/config/model.py | 6 ++++++ 1 file changed, 6 insertions(+) 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" From 227e003e7bdc06d01dfccbd95d17a1d2fa3a1ae8 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Fri, 23 Jan 2026 11:00:03 +0000 Subject: [PATCH 07/15] :recycle: refactor adapter, driver, and plugin commands to utilize EnvironmentExecutor for dependency management --- nb_cli/cli/commands/adapter.py | 74 +++++++++++----------------- nb_cli/cli/commands/driver.py | 88 +++++++++++++--------------------- nb_cli/cli/commands/plugin.py | 78 +++++++++++------------------- 3 files changed, 88 insertions(+), 152 deletions(-) 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.")) From 26b2a6ded00bf7859d17156215eef45a33b6c8f6 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Fri, 23 Jan 2026 11:22:47 +0000 Subject: [PATCH 08/15] :globe_with_meridians: update localization --- nb_cli/handlers/environment.py | 2 +- nb_cli/locale/zh_CN/LC_MESSAGES/nb-cli.po | 240 ++++++++++------------ 2 files changed, 107 insertions(+), 135 deletions(-) diff --git a/nb_cli/handlers/environment.py b/nb_cli/handlers/environment.py index b7c7cfe5..e8c1dafb 100644 --- a/nb_cli/handlers/environment.py +++ b/nb_cli/handlers/environment.py @@ -175,7 +175,7 @@ async def get( click.secho( _( "Warning: The current project uses {current!r} " - "but the available manager is {name}." + "but the available manager is {name!r}." ).format(current=current, name=name), fg="yellow", ) 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..93b60204 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-23 11:00+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,72 +472,62 @@ 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:245 msgid "Where to store the plugin?" msgstr "请输入插件存储位置:" @@ -821,21 +786,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:599 msgid "" "WARNING: Legacy configuration format detected.\n" "*** Use `nb upgrade-format` to upgrade to the new format." @@ -843,6 +808,13 @@ msgstr "" "警告: 检测到旧的项目格式.\n" "*** 使用 `nb upgrade-format` 升级至新格式." +#: nb_cli/handlers/environment.py:177 +#, python-brace-format +msgid "" +"Warning: The current project uses {current!r} but the available manager " +"is {name}." +msgstr "警告: 当前项目使用 {current!r} 但可用管理器为 {name!r}." + #: nb_cli/handlers/meta.py:93 msgid "Cannot find a valid Python interpreter." msgstr "无法找到可用的 Python 解释器." From c32b293619943d905122fdba4992cab33191bd95 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Sat, 24 Jan 2026 12:13:13 +0000 Subject: [PATCH 09/15] :sparkles: enhance dependency management with support for dependency groups --- nb_cli/config/parser.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/nb_cli/config/parser.py b/nb_cli/config/parser.py index 4e74cf7a..d5439101 100644 --- a/nb_cli/config/parser.py +++ b/nb_cli/config/parser.py @@ -324,16 +324,27 @@ 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 | Requirement) -> 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 = ( @@ -369,8 +380,14 @@ def add_dependency(self, *dependencies: str | PackageInfo | Requirement) -> None ) 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 | Requirement) -> None: if not dependencies: From 411843568287dc368f8e3766cd1af40745df8384 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Sat, 24 Jan 2026 12:20:16 +0000 Subject: [PATCH 10/15] :recycle: update doc for EnvironmentExecutor and remove unused parameter types --- nb_cli/handlers/environment.py | 68 ++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/nb_cli/handlers/environment.py b/nb_cli/handlers/environment.py index e8c1dafb..b6911e9d 100644 --- a/nb_cli/handlers/environment.py +++ b/nb_cli/handlers/environment.py @@ -165,7 +165,11 @@ async def get( Args: name: The name of the environment manager. cwd: The current working directory for the executor. - **kwargs: Additional keyword arguments to pass to the executor constructor. + 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. """ @@ -198,23 +202,23 @@ async def get( ) @abc.abstractmethod - async def init(self, extra_args: Sequence[str | bytes] = ()) -> None: + 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 | bytes] = ()) -> None: + 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 | bytes] = ()) -> None: + 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 | bytes] = () + self, *packages: Requirement, extra_args: Sequence[str] = (), dev: bool = False ) -> None: """Install packages into the environment. @@ -225,7 +229,7 @@ async def install( @abc.abstractmethod async def update( - self, *packages: Requirement, extra_args: Sequence[str | bytes] = () + self, *packages: Requirement, extra_args: Sequence[str] = () ) -> None: """Update packages in the environment. @@ -237,7 +241,7 @@ async def update( @abc.abstractmethod async def uninstall( - self, *packages: Requirement, extra_args: Sequence[str | bytes] = () + self, *packages: Requirement, extra_args: Sequence[str] = () ) -> None: """Uninstall packages from the environment. @@ -264,37 +268,39 @@ def __init__( ) -> None: super().__init__(cwd=cwd, stdin=stdin, stdout=stdout, stderr=stderr, env=env) - async def init(self, extra_args: Sequence[str | bytes] = ()) -> None: + 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 | bytes] = ()) -> None: + 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 | bytes] = ()) -> None: + 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 | bytes] = () + self, *packages: Requirement, extra_args: Sequence[str] = () ) -> None: 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 | bytes] = () + self, *packages: Requirement, extra_args: Sequence[str] = () ) -> None: - proc = await self.run("add", *(str(pkg) for pkg in packages), *extra_args) + 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 | bytes] = () + 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: @@ -318,23 +324,23 @@ def __init__( ) -> None: super().__init__(cwd=cwd, stdin=stdin, stdout=stdout, stderr=stderr, env=env) - async def init(self, extra_args: Sequence[str | bytes] = ()) -> None: + 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 | bytes] = ()) -> None: + 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 | bytes] = ()) -> None: + 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 | bytes] = () + self, *packages: Requirement, extra_args: Sequence[str] = () ) -> None: proc = await self.run("add", *(str(pkg) for pkg in packages), *extra_args) if await proc.wait() != 0: @@ -343,7 +349,7 @@ async def install( ) async def update( - self, *packages: Requirement, extra_args: Sequence[str | bytes] = () + 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: @@ -374,23 +380,23 @@ def __init__( ) -> None: super().__init__(cwd=cwd, stdin=stdin, stdout=stdout, stderr=stderr, env=env) - async def init(self, extra_args: Sequence[str | bytes] = ()) -> None: + 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 | bytes] = ()) -> None: + 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 | bytes] = ()) -> None: + 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 | bytes] = () + self, *packages: Requirement, extra_args: Sequence[str] = () ) -> None: proc = await self.run("add", *(str(pkg) for pkg in packages), *extra_args) if await proc.wait() != 0: @@ -399,7 +405,7 @@ async def install( ) async def update( - self, *packages: Requirement, extra_args: Sequence[str | bytes] = () + 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: @@ -408,7 +414,7 @@ async def update( ) async def uninstall( - self, *packages: Requirement, extra_args: Sequence[str | bytes] = () + 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: @@ -437,19 +443,19 @@ def __init__( self.executable = executable self.toml_manager = toml_manager - async def init(self, extra_args: Sequence[str | bytes] = ()) -> None: + async def init(self, extra_args: Sequence[str] = ()) -> None: pass # Pip does not require initialization. - async def lock(self, extra_args: Sequence[str | bytes] = ()) -> None: + 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 | bytes] = ()) -> None: + 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 | bytes] = () + self, *packages: Requirement, extra_args: Sequence[str] = () ) -> None: proc = await self.run( "-m", "pip", "install", *(str(pkg) for pkg in packages), *extra_args @@ -461,7 +467,7 @@ async def install( self.toml_manager.add_dependency(*packages) async def update( - self, *packages: Requirement, extra_args: Sequence[str | bytes] = () + self, *packages: Requirement, extra_args: Sequence[str] = () ) -> None: proc = await self.run( "-m", @@ -476,7 +482,7 @@ async def update( self.toml_manager.update_dependency(*packages) async def uninstall( - self, *packages: Requirement, extra_args: Sequence[str | bytes] = () + self, *packages: Requirement, extra_args: Sequence[str] = () ) -> None: free_packages = self.toml_manager.remove_dependency(*packages) if not free_packages: From 00a5c38a44ef349b8eda67a1073dbbc4990029f8 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Sat, 24 Jan 2026 12:21:23 +0000 Subject: [PATCH 11/15] :sparkles: enhance install method in environment executors to support development dependencies --- nb_cli/handlers/environment.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/nb_cli/handlers/environment.py b/nb_cli/handlers/environment.py index b6911e9d..70b9453b 100644 --- a/nb_cli/handlers/environment.py +++ b/nb_cli/handlers/environment.py @@ -284,8 +284,10 @@ async def sync(self, extra_args: Sequence[str] = ()) -> None: raise ProcessExecutionError("Failed to sync Uv environment.") async def install( - self, *packages: Requirement, extra_args: Sequence[str] = () + 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.") @@ -340,8 +342,10 @@ async def sync(self, extra_args: Sequence[str] = ()) -> None: raise ProcessExecutionError("Failed to sync PDM environment.") async def install( - self, *packages: Requirement, extra_args: Sequence[str] = () + 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( @@ -396,8 +400,10 @@ async def sync(self, extra_args: Sequence[str] = ()) -> None: raise ProcessExecutionError("Failed to sync Poetry environment.") async def install( - self, *packages: Requirement, extra_args: Sequence[str] = () + 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( @@ -455,7 +461,7 @@ async def sync(self, extra_args: Sequence[str] = ()) -> None: raise ProcessExecutionError("Failed to sync Pip environment.") async def install( - self, *packages: Requirement, extra_args: Sequence[str] = () + 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 @@ -464,7 +470,7 @@ async def install( raise ProcessExecutionError( "Failed to install packages in Pip environment." ) - self.toml_manager.add_dependency(*packages) + self.toml_manager.add_dependency(*packages, group="dev" if dev else None) async def update( self, *packages: Requirement, extra_args: Sequence[str] = () From fadadb129153b8d882345d45e6294e0209fffa9d Mon Sep 17 00:00:00 2001 From: worldmozara Date: Sat, 24 Jan 2026 13:16:16 +0000 Subject: [PATCH 12/15] :sparkles: enhance project creation process to support environment managers and improve dependency installation --- nb_cli/cli/commands/project.py | 144 ++++++++++++++++++++------------- 1 file changed, 89 insertions(+), 55 deletions(-) diff --git a/nb_cli/cli/commands/project.py b/nb_cli/cli/commands/project.py index f3db4adf..7f7524d0 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 environment 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") From cc4ea13ade907ecbce85f8b2ac6aa4f28b53016b Mon Sep 17 00:00:00 2001 From: worldmozara Date: Sat, 24 Jan 2026 13:21:23 +0000 Subject: [PATCH 13/15] :globe_with_meridians: add new translations --- nb_cli/cli/commands/project.py | 2 +- nb_cli/locale/zh_CN/LC_MESSAGES/nb-cli.po | 146 ++++++++++++---------- 2 files changed, 80 insertions(+), 68 deletions(-) diff --git a/nb_cli/cli/commands/project.py b/nb_cli/cli/commands/project.py index 7f7524d0..91b7e25d 100644 --- a/nb_cli/cli/commands/project.py +++ b/nb_cli/cli/commands/project.py @@ -345,7 +345,7 @@ async def create( if install_dependencies: try: manager = await ListPrompt( - _("Which environment manager would you like to use?"), + _("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: 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 93b60204..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: 2026-01-23 11:00+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" @@ -527,234 +527,246 @@ msgstr "插件名称:" msgid "Use nested plugin?" msgstr "使用嵌套插件?" -#: nb_cli/cli/commands/plugin.py:373 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:433 -msgid "" -"Failed to install dependencies! You should install the dependencies " -"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: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 "成功降级项目格式." @@ -800,7 +812,7 @@ msgstr "无法找到项目根目录! {config_file} 文件不存在." msgid "Using python: {python_path}" msgstr "使用 Python: {python_path}" -#: nb_cli/config/parser.py:599 +#: nb_cli/config/parser.py:616 msgid "" "WARNING: Legacy configuration format detected.\n" "*** Use `nb upgrade-format` to upgrade to the new format." @@ -808,11 +820,11 @@ msgstr "" "警告: 检测到旧的项目格式.\n" "*** 使用 `nb upgrade-format` 升级至新格式." -#: nb_cli/handlers/environment.py:177 +#: nb_cli/handlers/environment.py:181 #, python-brace-format msgid "" "Warning: The current project uses {current!r} but the available manager " -"is {name}." +"is {name!r}." msgstr "警告: 当前项目使用 {current!r} 但可用管理器为 {name!r}." #: nb_cli/handlers/meta.py:93 From 4ed413f22dc81121068d452899e66ec4096e8f79 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Sat, 24 Jan 2026 13:28:43 +0000 Subject: [PATCH 14/15] :recycle: rename parameter 'name' to 'manager_name' in EnvironmentExecutor subclasses due to conflict with ABCMeta --- nb_cli/handlers/environment.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/nb_cli/handlers/environment.py b/nb_cli/handlers/environment.py index 70b9453b..d764cc12 100644 --- a/nb_cli/handlers/environment.py +++ b/nb_cli/handlers/environment.py @@ -98,9 +98,9 @@ def __init__( self.executable = executable or self._executable self.env = env - def __init_subclass__(cls, /, *, name: str, **kwargs) -> None: - cls._executors[name] = cls - cls._executable = which(name) or name + 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]"] @@ -253,7 +253,7 @@ async def uninstall( ) -class UvEnvironmentExecutor(EnvironmentExecutor, name="uv"): +class UvEnvironmentExecutor(EnvironmentExecutor, manager_name="uv"): """Environment executor for Uv environment manager.""" def __init__( @@ -311,7 +311,7 @@ async def uninstall( ) -class PdmEnvironmentExecutor(EnvironmentExecutor, name="pdm"): +class PdmEnvironmentExecutor(EnvironmentExecutor, manager_name="pdm"): """Environment executor for PDM environment manager.""" def __init__( @@ -369,7 +369,7 @@ async def uninstall( ) -class PoetryEnvironmentExecutor(EnvironmentExecutor, name="poetry"): +class PoetryEnvironmentExecutor(EnvironmentExecutor, manager_name="poetry"): """Environment executor for Poetry environment manager.""" def __init__( @@ -429,7 +429,7 @@ async def uninstall( ) -class PipEnvironmentExecutor(EnvironmentExecutor, name="pip"): +class PipEnvironmentExecutor(EnvironmentExecutor, manager_name="pip"): """Environment executor for Pip environment manager.""" toml_manager: ConfigManager From bde2d9a63c6f5acc7ed4ac93b0b15abb804b69af Mon Sep 17 00:00:00 2001 From: worldmozara Date: Sat, 24 Jan 2026 13:32:41 +0000 Subject: [PATCH 15/15] :heavy_plus_sign: add pip to project dependencies --- pdm.lock | 13 ++++++++++++- pyproject.toml | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) 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]