Skip to content

Commit 86af164

Browse files
authored
Feature: 重构依赖管理部分以支持 extras 等依赖信息的精细管理 (#177)
* ✨ allow adding package extras for PackageInfo model * ✨ allow stripping extras in package matcher * ✨ make adapters/plugins installation work with package extras * ➕ add packaging for dependency requirements parsing * ♻️ refactor dependency management with packaging * 🐛 fix error when updating extras with installed packages
1 parent ea12d63 commit 86af164

File tree

8 files changed

+345
-134
lines changed

8 files changed

+345
-134
lines changed

nb_cli/cli/commands/adapter.py

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextlib
12
from typing import cast
23
from pathlib import Path
34

@@ -155,19 +156,40 @@ async def install(
155156
pip_args: list[str] | None,
156157
include_unpublished: bool = False,
157158
):
159+
extras: str | None = None
160+
if name and "[" in name:
161+
name, extras = name.split("[", 1)
162+
extras = extras.rstrip("]")
163+
158164
try:
159-
_installed = {
160-
(a.project_link, a.module_name) for a in await list_installed_adapters()
161-
}
162-
adapter = await find_exact_package(
163-
_("Adapter name to install:"),
164-
name,
165-
[
166-
a
167-
for a in await list_adapters(include_unpublished=include_unpublished)
168-
if (a.project_link, a.module_name) not in _installed
169-
],
170-
)
165+
_installed_adapters = await list_installed_adapters()
166+
is_installed = False
167+
adapter = None
168+
169+
if name is not None and extras is not None:
170+
with contextlib.suppress(RuntimeError):
171+
adapter = await find_exact_package(
172+
_("Adapter name to install:"),
173+
name,
174+
_installed_adapters,
175+
)
176+
is_installed = True
177+
178+
if not is_installed:
179+
_installed = {(a.project_link, a.module_name) for a in _installed_adapters}
180+
adapter = await find_exact_package(
181+
_("Adapter name to install:"),
182+
name,
183+
[
184+
a
185+
for a in await list_adapters(
186+
include_unpublished=include_unpublished
187+
)
188+
if (a.project_link, a.module_name) not in _installed
189+
],
190+
)
191+
192+
assert adapter is not None # confirmed by above logic
171193
except CancelledError:
172194
return
173195
except NoSelectablePackageError:
@@ -184,7 +206,8 @@ async def install(
184206
)
185207

186208
proc = await call_pip_install(
187-
adapter.as_dependency(not no_restrict_version), pip_args
209+
adapter.as_dependency(extras=extras, versioned=not no_restrict_version),
210+
pip_args,
188211
)
189212
if await proc.wait() != 0:
190213
click.secho(
@@ -208,7 +231,9 @@ async def install(
208231
)
209232

210233
try:
211-
GLOBAL_CONFIG.add_dependency(adapter)
234+
GLOBAL_CONFIG.add_dependency(
235+
adapter.as_dependency(extras=extras, versioned=not no_restrict_version)
236+
)
212237
except RuntimeError as e:
213238
click.echo(
214239
_("Failed to add adapter {adapter.name} to dependencies: {e}").format(
@@ -297,7 +322,17 @@ async def uninstall(name: str | None, pip_args: list[str] | None):
297322
click.echo(_("No installed adapter found to uninstall."))
298323
return
299324

325+
extras: str | None = None
326+
if name and "[" in name:
327+
name, extras = name.split("[", 1)
328+
extras = extras.rstrip("]")
329+
300330
try:
331+
if extras is not None:
332+
if not GLOBAL_CONFIG.remove_dependency(
333+
adapter.as_dependency(extras=extras)
334+
):
335+
return
301336
can_uninstall = GLOBAL_CONFIG.remove_adapter(adapter)
302337
if can_uninstall:
303338
GLOBAL_CONFIG.remove_dependency(adapter)

nb_cli/cli/commands/driver.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ async def install(
139139
_("Driver name to install:"),
140140
name,
141141
await list_drivers(include_unpublished=include_unpublished),
142+
no_extras=True,
142143
)
143144
except CancelledError:
144145
return

nb_cli/cli/commands/plugin.py

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextlib
12
from typing import cast
23
from pathlib import Path
34

@@ -155,19 +156,38 @@ async def install(
155156
pip_args: list[str] | None,
156157
include_unpublished: bool = False,
157158
):
159+
extras: str | None = None
160+
if name and "[" in name:
161+
name, extras = name.split("[", 1)
162+
extras = extras.rstrip("]")
163+
158164
try:
159-
_installed = {
160-
(p.project_link, p.module_name) for p in await list_installed_plugins()
161-
}
162-
plugin = await find_exact_package(
163-
_("Plugin name to install:"),
164-
name,
165-
[
166-
p
167-
for p in await list_plugins(include_unpublished=include_unpublished)
168-
if (p.project_link, p.module_name) not in _installed
169-
],
170-
)
165+
_installed_plugins = await list_installed_plugins()
166+
is_installed = False
167+
plugin = None
168+
169+
if name is not None and extras is not None:
170+
with contextlib.suppress(RuntimeError):
171+
plugin = await find_exact_package(
172+
_("Plugin name to install:"),
173+
name,
174+
_installed_plugins,
175+
)
176+
is_installed = True
177+
178+
if not is_installed:
179+
_installed = {(p.project_link, p.module_name) for p in _installed_plugins}
180+
plugin = await find_exact_package(
181+
_("Plugin name to install:"),
182+
name,
183+
[
184+
p
185+
for p in await list_plugins(include_unpublished=include_unpublished)
186+
if (p.project_link, p.module_name) not in _installed
187+
],
188+
)
189+
190+
assert plugin is not None # confirmed by above logic
171191
except CancelledError:
172192
return
173193
except NoSelectablePackageError:
@@ -184,7 +204,7 @@ async def install(
184204
)
185205

186206
proc = await call_pip_install(
187-
plugin.as_dependency(not no_restrict_version), pip_args
207+
plugin.as_dependency(extras=extras, versioned=not no_restrict_version), pip_args
188208
)
189209
if await proc.wait() != 0:
190210
click.secho(
@@ -208,7 +228,9 @@ async def install(
208228
)
209229

210230
try:
211-
GLOBAL_CONFIG.add_dependency(plugin)
231+
GLOBAL_CONFIG.add_dependency(
232+
plugin.as_dependency(extras=extras, versioned=not no_restrict_version)
233+
)
212234
except RuntimeError as e:
213235
click.echo(
214236
_("Failed to add plugin {plugin.name} to dependencies: {e}").format(
@@ -297,7 +319,15 @@ async def uninstall(name: str | None, pip_args: list[str] | None):
297319
click.echo(_("No installed plugin found to uninstall."))
298320
return
299321

322+
extras: str | None = None
323+
if name and "[" in name:
324+
name, extras = name.split("[", 1)
325+
extras = extras.rstrip("]")
326+
300327
try:
328+
if extras is not None:
329+
if not GLOBAL_CONFIG.remove_dependency(plugin.as_dependency(extras=extras)):
330+
return
301331
can_uninstall = GLOBAL_CONFIG.remove_plugin(plugin)
302332
if can_uninstall:
303333
GLOBAL_CONFIG.remove_dependency(plugin)

nb_cli/cli/utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ def __call__(self, x: Adapter | Plugin | Driver, *, value: str) -> bool: ...
6262
}
6363

6464

65-
async def find_exact_package(question: str, name: str | None, packages: list[T]) -> T:
65+
async def find_exact_package(
66+
question: str, name: str | None, packages: list[T], *, no_extras: bool = False
67+
) -> T:
6668
if name is None:
6769
if not packages:
6870
raise NoSelectablePackageError("No packages available to select.")
@@ -82,6 +84,9 @@ async def find_exact_package(question: str, name: str | None, packages: list[T])
8284
).prompt_async(style=CLI_DEFAULT_STYLE)
8385
).data
8486

87+
if not no_extras and "[" in name:
88+
name = name.split("[", 1)[0].strip()
89+
8590
if exact_packages := [
8691
p for p in packages if name in {p.name, p.module_name, p.project_link}
8792
]:

nb_cli/config/model.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,17 @@ class PackageInfo(SimpleInfo):
2828
def time_serializer(self, dt: datetime):
2929
return dt.isoformat()
3030

31-
def as_dependency(self, versioned: bool = True) -> str:
31+
def as_dependency(
32+
self, *, extras: str | None = None, versioned: bool = True
33+
) -> str:
34+
if extras:
35+
if "[" in self.project_link:
36+
raise ValueError("Project link already contains extras.")
37+
return (
38+
f"{self.project_link}[{extras}]>={self.version}"
39+
if versioned
40+
else f"{self.project_link}[{extras}]"
41+
)
3242
return (
3343
f"{self.project_link}>={self.version}" if versioned else self.project_link
3444
)

0 commit comments

Comments
 (0)