mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
fix(code): --reinstall on /install so upgrades rebuild a clean env (#4196)
`/install` now reinstalls the dcode tool cleanly, fixing server-startup `ImportError`s caused by half-updated environments after adding an extra or package. --- When you run `/install <extra>` (e.g. `/install fireworks`) or `/install <pkg> --package`, dcode runs `uv tool install -U 'deepagents-code[...]'`. The `-U` upgrades the tool **in place**, on top of the copy that's currently running. An in-place upgrade can leave stale files behind — for example an old `tools.py` (or its cached `.pyc`) from the previous version. You then end up with a tool environment that's a mix of old and new files. The next time the server starts (after `/restart`), the new `server_graph.py` tries to import something the stale `tools.py` doesn't have, and the server dies on startup. The fix is small: pass `--reinstall` so uv rebuilds the tool environment from scratch instead of patching it in place. Every file then matches the freshly resolved version, so adding a model/extra can't leave the install in a broken state. This is the same thing the manual workaround (`uv tool install --reinstall -U ...`) does, now baked into the `/install` flows. The manual-command hints we show users are updated to match. Package installs now use the same receipt-preserving reinstall path as extras, so adding a custom provider package keeps existing extras, `--with` packages, the uv-managed Python interpreter, and prerelease channel instead of rebuilding a narrower tool environment. Made by [Open SWE](https://openswe.vercel.app/agents/e196cb0c-bde5-c559-1675-3226784d7178) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
@@ -3461,11 +3461,18 @@ class DeepAgentsApp(App):
|
||||
)
|
||||
else:
|
||||
from deepagents_code.extras_info import ExtrasIntrospectionError
|
||||
from deepagents_code.update_check import install_package_command
|
||||
from deepagents_code.update_check import (
|
||||
ToolRequirementIntrospectionError,
|
||||
install_package_command,
|
||||
)
|
||||
|
||||
try:
|
||||
install_cmd = install_package_command(missing.package)
|
||||
except (ValueError, ExtrasIntrospectionError) as exc:
|
||||
except (
|
||||
ValueError,
|
||||
ExtrasIntrospectionError,
|
||||
ToolRequirementIntrospectionError,
|
||||
) as exc:
|
||||
logger.debug(
|
||||
"install_package_command failed; falling back to "
|
||||
"manual hint: %s",
|
||||
@@ -4313,7 +4320,8 @@ class DeepAgentsApp(App):
|
||||
"""Handle the `/install <extra>` slash command.
|
||||
|
||||
Adds an optional extra (e.g. `quickjs`, `daytona`) to the installed
|
||||
dcode tool by re-running `uv tool install -U 'deepagents-code[<extra>]'`.
|
||||
dcode tool by re-running
|
||||
`uv tool install --reinstall -U 'deepagents-code[<extra>]'`.
|
||||
Refuses unknown extras unless the user passes a `--force` token.
|
||||
|
||||
Args:
|
||||
@@ -6975,7 +6983,8 @@ class DeepAgentsApp(App):
|
||||
AppMessage(
|
||||
"The `langsmith` package is not installed. "
|
||||
"Install it with "
|
||||
"`uv tool install -U deepagents-code --with langsmith` "
|
||||
"`uv tool install --reinstall -U deepagents-code "
|
||||
"--with langsmith` "
|
||||
"to enable `/trace`.",
|
||||
),
|
||||
)
|
||||
@@ -11799,7 +11808,7 @@ class DeepAgentsApp(App):
|
||||
def _ensure_restart_prompt_loaded() -> None:
|
||||
"""Load the restart-prompt modal before any in-place self-upgrade.
|
||||
|
||||
`/install` runs `uv tool install -U 'deepagents-code[...]'`, which
|
||||
`/install` runs `uv tool install --reinstall -U 'deepagents-code[...]'`, which
|
||||
rewrites deepagents-code's own on-disk package tree while this process
|
||||
is running. Modules already in `sys.modules` keep working from memory,
|
||||
but a *first* import after the rewrite reads the mutated (or
|
||||
@@ -11836,11 +11845,11 @@ class DeepAgentsApp(App):
|
||||
redundant hint:
|
||||
|
||||
- Owned + idle: show the prompt (its button is the call to action). If
|
||||
the prompt can't be shown, fall back to a `/restart` hint.
|
||||
the prompt can't be shown, fall back to a `/restart` hint.
|
||||
- Owned + busy/connecting: a restart cancels in-flight work, so point
|
||||
at `/restart` for once the current task finishes.
|
||||
at `/restart` for once the current task finishes.
|
||||
- No owned subprocess (remote server): `/restart` can't respawn it, so a
|
||||
full relaunch is the only way to load the package.
|
||||
full relaunch is the only way to load the package.
|
||||
|
||||
Args:
|
||||
label: Installed extra/package name, surfaced in the prompt title.
|
||||
@@ -11866,8 +11875,9 @@ class DeepAgentsApp(App):
|
||||
try:
|
||||
from deepagents_code.widgets.restart_prompt import RestartPromptScreen
|
||||
except ModuleNotFoundError:
|
||||
# `/install` runs `uv tool install -U 'deepagents-code[...]'`, which
|
||||
# can rewrite deepagents-code's own on-disk package tree mid-session
|
||||
# `/install` runs `uv tool install --reinstall -U
|
||||
# 'deepagents-code[...]'`, which can rewrite deepagents-code's own
|
||||
# on-disk package tree mid-session
|
||||
# (see `_ensure_restart_prompt_loaded`). A first import of the modal
|
||||
# here may then fail with `ModuleNotFoundError`. Degrade to the
|
||||
# manual `/restart` hint instead of crashing the TUI. The catch is
|
||||
|
||||
@@ -3426,11 +3426,18 @@ def _create_model_via_init(
|
||||
)
|
||||
else:
|
||||
from deepagents_code.extras_info import ExtrasIntrospectionError
|
||||
from deepagents_code.update_check import install_package_command
|
||||
from deepagents_code.update_check import (
|
||||
ToolRequirementIntrospectionError,
|
||||
install_package_command,
|
||||
)
|
||||
|
||||
try:
|
||||
install_cmd = install_package_command(package)
|
||||
except (ValueError, ExtrasIntrospectionError) as exc:
|
||||
except (
|
||||
ValueError,
|
||||
ExtrasIntrospectionError,
|
||||
ToolRequirementIntrospectionError,
|
||||
) as exc:
|
||||
logger.debug(
|
||||
"install_package_command failed; falling back to "
|
||||
"manual hint: %s",
|
||||
|
||||
@@ -2372,7 +2372,7 @@ def cli_main() -> None:
|
||||
except ImportError as exc:
|
||||
msg = (
|
||||
f"ACP dependencies not available: {exc}\n"
|
||||
"Install with: uv tool install -U deepagents-code "
|
||||
"Install with: uv tool install --reinstall -U deepagents-code "
|
||||
"--with deepagents-acp\n"
|
||||
)
|
||||
sys.stderr.write(msg)
|
||||
|
||||
@@ -291,7 +291,8 @@ def get_latest_version(
|
||||
except ImportError:
|
||||
logger.warning(
|
||||
"requests package not installed — update checks disabled. "
|
||||
"Install with: uv tool install -U deepagents-code --with requests"
|
||||
"Install with: uv tool install --reinstall -U deepagents-code "
|
||||
"--with requests"
|
||||
)
|
||||
return cached_version
|
||||
|
||||
@@ -1876,6 +1877,8 @@ def _uv_tool_install_command(
|
||||
include_prereleases: bool | None,
|
||||
distribution_name: str,
|
||||
extras_to_add: Iterable[str] = (),
|
||||
with_packages_to_add: Iterable[str] = (),
|
||||
reinstall: bool = False,
|
||||
) -> str:
|
||||
"""Return the receipt-preserving `uv tool install -U` command.
|
||||
|
||||
@@ -1885,6 +1888,19 @@ def _uv_tool_install_command(
|
||||
`None`, follows the installed version's channel.
|
||||
distribution_name: Name of the installed distribution to inspect.
|
||||
extras_to_add: Extra names to merge with already-installed extras.
|
||||
with_packages_to_add: Package names to merge with the receipt's existing
|
||||
`--with` packages. Names already present (compared canonically) are
|
||||
not duplicated; genuinely new names are appended after the preserved
|
||||
ones. Callers must validate these names before passing them — the
|
||||
builder only `shlex.quote`-s them.
|
||||
reinstall: When `True`, add `--reinstall` so uv rebuilds the tool
|
||||
environment from scratch instead of patching it in place. An
|
||||
in-place `-U` upgrade can leave stale files behind (e.g. an old
|
||||
`tools.py` or its cached bytecode), which has been observed to
|
||||
produce a half-updated env that crashes the next server start with
|
||||
an `ImportError`; the preserved `--python` interpreter and `--with`
|
||||
packages still apply, so the rebuild keeps the existing tool
|
||||
context.
|
||||
|
||||
Raises:
|
||||
ExtrasIntrospectionError: If a metadata-sourced extra name fails PEP 508
|
||||
@@ -1906,12 +1922,17 @@ def _uv_tool_install_command(
|
||||
except ValueError as exc:
|
||||
msg = f"Distribution metadata yielded an invalid extra name: {exc}"
|
||||
raise ExtrasIntrospectionError(msg) from exc
|
||||
cmd = "uv tool install -U"
|
||||
cmd = "uv tool install --reinstall -U" if reinstall else "uv tool install -U"
|
||||
python = _uv_tool_python()
|
||||
if python is not None:
|
||||
cmd += f" --python {shlex.quote(python)}"
|
||||
cmd += f" {requirement}"
|
||||
with_packages = _uv_tool_with_packages(distribution_name=distribution_name)
|
||||
with_packages = list(_uv_tool_with_packages(distribution_name=distribution_name))
|
||||
known = {canonicalize_name(package) for package in with_packages}
|
||||
for package in with_packages_to_add:
|
||||
if canonicalize_name(package) not in known:
|
||||
with_packages.append(package)
|
||||
known.add(canonicalize_name(package))
|
||||
for package in with_packages:
|
||||
cmd += f" --with {shlex.quote(package)}"
|
||||
if _resolve_include_prereleases(include_prereleases):
|
||||
@@ -2047,30 +2068,42 @@ def install_package_command(
|
||||
|
||||
The result is built for *execution* (via `perform_install_package`), not for
|
||||
display — surfacing raw `uv tool` invocations to the user is intentionally
|
||||
avoided. `package` is validated and then `shlex.quote`-d: the validation
|
||||
already blocks shell metacharacters, so the quoting is defense in depth that
|
||||
keeps the command safe even if the pattern is later loosened.
|
||||
avoided. `package` is validated here against PEP 508 grammar and then
|
||||
`shlex.quote`-d by the shared builder: the validation already blocks shell
|
||||
metacharacters, so the quoting is defense in depth that keeps the command
|
||||
safe even if the pattern is later loosened.
|
||||
|
||||
Already-installed extras are folded into the `deepagents-code[...]`
|
||||
requirement via the shared `_dcode_extras_requirement` helper, the same way
|
||||
`install_extras_command` builds its requirement. Without this the reinstall
|
||||
would replace the tool with a plain `deepagents-code`, silently dropping any
|
||||
extras the user added through `/install <extra>`.
|
||||
Delegates to `_uv_tool_install_command` (the same builder the extras path
|
||||
uses), passing the new package as a `--with` requirement. That builder folds
|
||||
already-installed extras into the `deepagents-code[...]` requirement, and
|
||||
preserves the uv-managed Python interpreter, the receipt's existing `--with`
|
||||
packages, and the installed pre-release channel. Without this, reinstalling
|
||||
to add a second package would replace the tool with a plain `deepagents-code`
|
||||
(dropping extras the user added through `/install <extra>`), rebuild with
|
||||
only the newest `--with` package (dropping previously configured custom
|
||||
providers), or silently downgrade a pre-release install to the latest stable.
|
||||
|
||||
Like the extras path (`_install_extra_uv_tool_command`), passes
|
||||
`reinstall=True` so the upgrade rebuilds the tool environment cleanly; see
|
||||
`_uv_tool_install_command`'s `reinstall` parameter for why an in-place
|
||||
upgrade is unsafe.
|
||||
|
||||
Args:
|
||||
package: Package name to install into the existing tool environment.
|
||||
distribution_name: Name of the installed distribution to inspect for
|
||||
already-installed extras.
|
||||
already-installed extras and uv receipt requirements.
|
||||
|
||||
Returns:
|
||||
Shell command string suitable for execution via the shell.
|
||||
|
||||
Raises:
|
||||
ExtrasIntrospectionError: If installed extras cannot be determined
|
||||
safely from distribution metadata (refused rather than risk
|
||||
dropping them).
|
||||
ValueError: If `package` or any already-installed extra fails PEP 508
|
||||
validation.
|
||||
ValueError: If `package` fails PEP 508 validation.
|
||||
|
||||
Propagates `ExtrasIntrospectionError` if installed extras cannot be
|
||||
determined safely from distribution metadata (or a metadata-sourced extra
|
||||
name fails PEP 508 validation), and `ToolRequirementIntrospectionError` if
|
||||
the uv tool receipt's interpreter or `--with` packages cannot be determined
|
||||
safely.
|
||||
"""
|
||||
if not _PACKAGE_NAME_RE.fullmatch(package):
|
||||
msg = (
|
||||
@@ -2078,19 +2111,14 @@ def install_package_command(
|
||||
f"({_PACKAGE_NAME_RE.pattern})"
|
||||
)
|
||||
raise ValueError(msg)
|
||||
from deepagents_code.extras_info import (
|
||||
ExtrasIntrospectionError,
|
||||
installed_extra_names,
|
||||
return _uv_tool_install_command(
|
||||
version=None,
|
||||
include_prereleases=None,
|
||||
distribution_name=distribution_name,
|
||||
with_packages_to_add=(package,),
|
||||
reinstall=True,
|
||||
)
|
||||
|
||||
try:
|
||||
extras = installed_extra_names(distribution_name, strict=True)
|
||||
except ExtrasIntrospectionError as exc:
|
||||
msg = str(exc)
|
||||
raise ExtrasIntrospectionError(msg) from exc
|
||||
requirement = _dcode_extras_requirement(extras)
|
||||
return f"uv tool install -U {requirement} --with {shlex.quote(package)}"
|
||||
|
||||
|
||||
def install_extras_command(extras: Iterable[str]) -> str:
|
||||
"""Return the install-script command that installs dcode extras.
|
||||
@@ -2183,6 +2211,10 @@ def _install_extra_uv_tool_command(
|
||||
) -> str:
|
||||
"""Return the receipt-preserving uv command that installs one dcode extra.
|
||||
|
||||
Passes `reinstall=True` so the upgrade rebuilds the tool environment from
|
||||
scratch rather than patching it in place; see `_uv_tool_install_command`'s
|
||||
`reinstall` parameter for why an in-place upgrade is unsafe.
|
||||
|
||||
Args:
|
||||
extra: The extra name to add. Validated against PEP 508 grammar before
|
||||
interpolation into the shell command.
|
||||
@@ -2208,6 +2240,7 @@ def _install_extra_uv_tool_command(
|
||||
include_prereleases=None,
|
||||
distribution_name=distribution_name,
|
||||
extras_to_add=(extra,),
|
||||
reinstall=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -2248,11 +2281,11 @@ async def perform_install_extra(
|
||||
) -> tuple[bool, str]:
|
||||
"""Add `extra` to the installed dcode tool environment.
|
||||
|
||||
Runs `uv tool install -U 'deepagents-code[<extras>]'`, preserving any
|
||||
extras that are already installed. Editable installs are refused — the
|
||||
caller should rerun their `uv tool install --editable` command with `--with
|
||||
'deepagents-code[<extra>]'` added so the extra is resolved against the
|
||||
editable source.
|
||||
Runs `uv tool install --reinstall -U 'deepagents-code[<extras>]'`,
|
||||
preserving any extras that are already installed. Editable installs are
|
||||
refused — the caller should rerun their `uv tool install --editable` command
|
||||
with `--with 'deepagents-code[<extra>]'` added so the extra is resolved
|
||||
against the editable source.
|
||||
|
||||
Args:
|
||||
extra: The extra name to install. Must satisfy `is_valid_extra_name`;
|
||||
@@ -2319,10 +2352,11 @@ async def perform_install_package(
|
||||
) -> tuple[bool, str]:
|
||||
"""Add an arbitrary `package` to the installed dcode tool environment.
|
||||
|
||||
Runs `uv tool install -U 'deepagents-code[<extras>]' --with <package>`, the
|
||||
escape hatch for a provider whose package is not a `deepagents-code` extra
|
||||
(e.g. a custom or in-house `class_path` model). Already-installed extras are
|
||||
preserved so the reinstall does not drop them. Editable installs are refused
|
||||
Runs `uv tool install --reinstall -U 'deepagents-code[<extras>]' --with
|
||||
<package>`, the escape hatch for a provider whose package is not a
|
||||
`deepagents-code` extra (e.g. a custom or in-house `class_path` model).
|
||||
Already-installed extras are preserved so the reinstall does not drop them.
|
||||
Editable installs are refused
|
||||
— the caller should rerun their `uv tool install --editable` command with
|
||||
`--with <package>` added so it resolves against the editable source.
|
||||
|
||||
@@ -2378,12 +2412,14 @@ async def perform_install_package(
|
||||
cmd = install_package_command(package)
|
||||
except ValueError as exc:
|
||||
return False, f"{type(exc).__name__}: {exc}"
|
||||
except ExtrasIntrospectionError as exc:
|
||||
except (ExtrasIntrospectionError, ToolRequirementIntrospectionError) as exc:
|
||||
# Distinct from a malformed package name: the running distribution's own
|
||||
# metadata could not be read or parsed. Leave a breadcrumb so the cause
|
||||
# is recoverable from logs, even though the user message is unchanged.
|
||||
# metadata, or the uv tool receipt, could not be read or parsed. Leave a
|
||||
# breadcrumb so the cause is recoverable from logs, even though the user
|
||||
# message is unchanged.
|
||||
logger.warning(
|
||||
"Could not introspect installed extras for package install of %r",
|
||||
"Could not introspect installed extras or uv receipt for package "
|
||||
"install of %r",
|
||||
package,
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
@@ -8819,12 +8819,24 @@ class TestDeferredActions:
|
||||
assert "/model google_vertexai:<model>" in rendered
|
||||
|
||||
async def test_server_failure_missing_unknown_package_shows_uv_command(
|
||||
self,
|
||||
self, tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
"""Manual fallback should include the default uv tool command."""
|
||||
"""Manual fallback should include the default uv tool command.
|
||||
|
||||
`install_package_command` reads the uv tool receipt to preserve the
|
||||
interpreter and existing `--with` packages, so the receipt must be
|
||||
isolated to a temporary tool root or the hint degrades to the manual
|
||||
fallback (see the sibling unreadable-receipt test).
|
||||
"""
|
||||
from deepagents_code.model_config import MissingProviderPackageError
|
||||
from deepagents_code.widgets.messages import ErrorMessage
|
||||
|
||||
tmp_path.joinpath("uv-receipt.toml").write_text(
|
||||
'[tool]\nrequirements = [{ name = "deepagents-code" }]\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
|
||||
app = DeepAgentsApp()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
@@ -8855,8 +8867,60 @@ class TestDeferredActions:
|
||||
assert isinstance(widget, ErrorMessage)
|
||||
rendered = str(widget._content)
|
||||
assert (
|
||||
"uv tool install -U deepagents-code --with langchain-custom_provider"
|
||||
in rendered
|
||||
"uv tool install --reinstall -U deepagents-code "
|
||||
"--with langchain-custom_provider" in rendered
|
||||
)
|
||||
assert "/model custom_provider:<model>" in rendered
|
||||
|
||||
async def test_server_failure_unknown_package_unreadable_receipt_manual(
|
||||
self, tmp_path: Path, monkeypatch
|
||||
) -> None:
|
||||
"""An unreadable uv receipt degrades the hint to a manual instruction.
|
||||
|
||||
`install_package_command` raises `ToolRequirementIntrospectionError`
|
||||
when the uv tool receipt is missing — a realistic state for a tool whose
|
||||
receipt records an unsupported `--with` source. The failure-render path
|
||||
must catch it and surface an actionable manual hint rather than crash
|
||||
while building the recovery message.
|
||||
"""
|
||||
from deepagents_code.model_config import MissingProviderPackageError
|
||||
from deepagents_code.widgets.messages import ErrorMessage
|
||||
|
||||
# tmp_path intentionally has no uv-receipt.toml, so the receipt read
|
||||
# raises ToolRequirementIntrospectionError.
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
|
||||
app = DeepAgentsApp()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
app._server_kwargs = {"model_name": "custom_provider:fake"}
|
||||
app._connecting = True
|
||||
|
||||
error = MissingProviderPackageError(
|
||||
"Missing package for provider 'custom_provider'.",
|
||||
provider="custom_provider",
|
||||
package="langchain-custom_provider",
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"deepagents_code.extras_info.extra_for_package",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"deepagents_code.extras_info.installed_extra_names",
|
||||
return_value=set(),
|
||||
),
|
||||
):
|
||||
app.on_deep_agents_app_server_start_failed(
|
||||
DeepAgentsApp.ServerStartFailed(error=error)
|
||||
)
|
||||
await pilot.pause()
|
||||
|
||||
widget = app._startup_failure_widget
|
||||
assert isinstance(widget, ErrorMessage)
|
||||
rendered = str(widget._content)
|
||||
assert (
|
||||
"install the `langchain-custom_provider` package manually" in rendered
|
||||
)
|
||||
assert "/model custom_provider:<model>" in rendered
|
||||
|
||||
|
||||
@@ -3905,8 +3905,20 @@ class TestCreateModelViaInitImportError:
|
||||
_create_model_via_init("nemotron", "nvidia", {})
|
||||
|
||||
@patch("langchain.chat_models.init_chat_model")
|
||||
def test_unknown_provider_fallback_package_name(self, mock_init: Mock) -> None:
|
||||
"""Unknown provider falls back to langchain-{provider} package name."""
|
||||
def test_unknown_provider_fallback_package_name(
|
||||
self, mock_init: Mock, tmp_path, monkeypatch
|
||||
) -> None:
|
||||
"""Unknown provider falls back to langchain-{provider} package name.
|
||||
|
||||
`install_package_command` reads the uv tool receipt, so it is isolated to
|
||||
a temporary tool root here; otherwise the hint degrades to the manual
|
||||
fallback (see the sibling unreadable-receipt test).
|
||||
"""
|
||||
tmp_path.joinpath("uv-receipt.toml").write_text(
|
||||
'[tool]\nrequirements = [{ name = "deepagents-code" }]\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
mock_init.side_effect = ImportError("no module")
|
||||
with (
|
||||
patch("importlib.util.find_spec", return_value=None),
|
||||
@@ -3917,13 +3929,40 @@ class TestCreateModelViaInitImportError:
|
||||
pytest.raises(
|
||||
ModelConfigError,
|
||||
match=(
|
||||
"Install with: uv tool install -U deepagents-code "
|
||||
"Install with: uv tool install --reinstall -U deepagents-code "
|
||||
"--with langchain-custom_provider"
|
||||
),
|
||||
),
|
||||
):
|
||||
_create_model_via_init("some-model", "custom_provider", {})
|
||||
|
||||
@patch("langchain.chat_models.init_chat_model")
|
||||
def test_unknown_provider_receipt_failure_falls_back_to_manual(
|
||||
self, mock_init: Mock, tmp_path, monkeypatch
|
||||
) -> None:
|
||||
"""An unreadable uv receipt degrades to the manual-install hint.
|
||||
|
||||
Exercises the `ToolRequirementIntrospectionError` arm of the fallback:
|
||||
`install_package_command` reads the uv tool receipt, and a missing
|
||||
receipt must surface an actionable message instead of letting the error
|
||||
leak out of hint construction.
|
||||
"""
|
||||
# tmp_path has no uv-receipt.toml, so the receipt read raises.
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
mock_init.side_effect = ImportError("no module")
|
||||
with (
|
||||
patch("importlib.util.find_spec", return_value=None),
|
||||
patch(
|
||||
"deepagents_code.extras_info.installed_extra_names",
|
||||
return_value=set(),
|
||||
),
|
||||
pytest.raises(
|
||||
ModelConfigError,
|
||||
match="Install the 'langchain-custom_provider' package manually",
|
||||
),
|
||||
):
|
||||
_create_model_via_init("some-model", "custom_provider", {})
|
||||
|
||||
@patch("langchain.chat_models.init_chat_model")
|
||||
def test_unknown_provider_introspection_failure_falls_back_to_manual(
|
||||
self, mock_init: Mock
|
||||
|
||||
@@ -2558,7 +2558,7 @@ class TestInstallExtraCommand:
|
||||
)
|
||||
|
||||
assert install_extra_recovery_command("quickjs") == (
|
||||
"uv tool install -U --python '/opt/Python 3.13/bin/python' "
|
||||
"uv tool install --reinstall -U --python '/opt/Python 3.13/bin/python' "
|
||||
"'deepagents-code[nvidia,quickjs]' --with langchain-custom"
|
||||
)
|
||||
|
||||
@@ -2618,7 +2618,7 @@ class TestInstallExtraCommand:
|
||||
_install_extra_uv_tool_command(
|
||||
"baseten", distribution_name="deepagents-code"
|
||||
)
|
||||
== "uv tool install -U 'deepagents-code[baseten,nvidia]'"
|
||||
== "uv tool install --reinstall -U 'deepagents-code[baseten,nvidia]'"
|
||||
)
|
||||
|
||||
def test_uv_install_extra_command_dedupes_existing_extra(
|
||||
@@ -2639,7 +2639,7 @@ class TestInstallExtraCommand:
|
||||
_install_extra_uv_tool_command(
|
||||
"nvidia", distribution_name="deepagents-code"
|
||||
)
|
||||
== "uv tool install -U 'deepagents-code[nvidia]'"
|
||||
== "uv tool install --reinstall -U 'deepagents-code[nvidia]'"
|
||||
)
|
||||
|
||||
def test_uv_install_extra_command_drops_composite_extras(
|
||||
@@ -2665,7 +2665,7 @@ class TestInstallExtraCommand:
|
||||
_install_extra_uv_tool_command(
|
||||
"baseten", distribution_name="deepagents-code"
|
||||
)
|
||||
== "uv tool install -U 'deepagents-code[baseten,nvidia]'"
|
||||
== "uv tool install --reinstall -U 'deepagents-code[baseten,nvidia]'"
|
||||
)
|
||||
|
||||
def test_uv_install_extra_command_preserves_receipt_python_and_with_packages(
|
||||
@@ -2691,7 +2691,7 @@ class TestInstallExtraCommand:
|
||||
)
|
||||
|
||||
assert command == (
|
||||
"uv tool install -U --python '/opt/Python 3.13/bin/python' "
|
||||
"uv tool install --reinstall -U --python '/opt/Python 3.13/bin/python' "
|
||||
"'deepagents-code[baseten,nvidia]' --with langchain-custom"
|
||||
)
|
||||
|
||||
@@ -2730,37 +2730,43 @@ class TestInstallPackageCommand:
|
||||
|
||||
def test_basic_no_extras(self, tmp_path, monkeypatch) -> None:
|
||||
"""Clean metadata with no installed extras yields a plain requirement."""
|
||||
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }')
|
||||
_write_dist_info(
|
||||
tmp_path,
|
||||
"deepagents-code",
|
||||
requires=('definitely-absent-dcode-test-quickjs-xyz; extra == "quickjs"',),
|
||||
)
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
|
||||
assert (
|
||||
install_package_command(
|
||||
"langchain-custom", distribution_name="deepagents-code"
|
||||
)
|
||||
== "uv tool install -U deepagents-code --with langchain-custom"
|
||||
== "uv tool install --reinstall -U deepagents-code --with langchain-custom"
|
||||
)
|
||||
|
||||
def test_allows_pep508_name_separators(self, tmp_path, monkeypatch) -> None:
|
||||
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }')
|
||||
_write_dist_info(
|
||||
tmp_path,
|
||||
"deepagents-code",
|
||||
requires=('definitely-absent-dcode-test-quickjs-xyz; extra == "quickjs"',),
|
||||
)
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
|
||||
assert (
|
||||
install_package_command(
|
||||
"langchain.custom_provider", distribution_name="deepagents-code"
|
||||
)
|
||||
== "uv tool install -U deepagents-code --with langchain.custom_provider"
|
||||
== "uv tool install --reinstall -U deepagents-code "
|
||||
"--with langchain.custom_provider"
|
||||
)
|
||||
|
||||
def test_preserves_installed_extras(self, tmp_path, monkeypatch) -> None:
|
||||
"""Adding a package keeps already-installed extras selected."""
|
||||
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }')
|
||||
_write_dist_info(tmp_path, "definitely-present-dcode-test-nvidia")
|
||||
_write_dist_info(
|
||||
tmp_path,
|
||||
@@ -2770,6 +2776,7 @@ class TestInstallPackageCommand:
|
||||
'definitely-absent-dcode-test-baseten-xyz; extra == "baseten"',
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
|
||||
assert installed_extra_names("deepagents-code") == {"nvidia"}
|
||||
@@ -2777,9 +2784,158 @@ class TestInstallPackageCommand:
|
||||
install_package_command(
|
||||
"langchain-custom", distribution_name="deepagents-code"
|
||||
)
|
||||
== "uv tool install -U 'deepagents-code[nvidia]' --with langchain-custom"
|
||||
== "uv tool install --reinstall -U 'deepagents-code[nvidia]' "
|
||||
"--with langchain-custom"
|
||||
)
|
||||
|
||||
def test_preserves_receipt_python_and_with_packages(
|
||||
self, tmp_path, monkeypatch
|
||||
) -> None:
|
||||
"""Adding a package keeps uv receipt interpreter and `--with` packages."""
|
||||
_write_uv_receipt(
|
||||
tmp_path,
|
||||
'{ name = "deepagents-code" }, { name = "langchain-first" }',
|
||||
python="/opt/Python 3.13/bin/python",
|
||||
)
|
||||
_write_dist_info(tmp_path, "definitely-present-dcode-test-nvidia")
|
||||
_write_dist_info(
|
||||
tmp_path,
|
||||
"deepagents-code",
|
||||
requires=('definitely-present-dcode-test-nvidia; extra == "nvidia"',),
|
||||
)
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
|
||||
command = install_package_command(
|
||||
"langchain-second", distribution_name="deepagents-code"
|
||||
)
|
||||
|
||||
assert command == (
|
||||
"uv tool install --reinstall -U --python '/opt/Python 3.13/bin/python' "
|
||||
"'deepagents-code[nvidia]' --with langchain-first "
|
||||
"--with langchain-second"
|
||||
)
|
||||
|
||||
def test_does_not_duplicate_existing_receipt_package(
|
||||
self, tmp_path, monkeypatch
|
||||
) -> None:
|
||||
"""Reinstalling an existing package does not emit duplicate `--with` args."""
|
||||
_write_uv_receipt(
|
||||
tmp_path,
|
||||
'{ name = "deepagents-code" }, { name = "langchain-custom" }',
|
||||
)
|
||||
_write_dist_info(tmp_path, "deepagents-code")
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
|
||||
command = install_package_command(
|
||||
"LangChain_Custom", distribution_name="deepagents-code"
|
||||
)
|
||||
|
||||
assert (
|
||||
command
|
||||
== "uv tool install --reinstall -U deepagents-code --with langchain-custom"
|
||||
)
|
||||
|
||||
def test_preserves_prerelease_channel(self, tmp_path, monkeypatch) -> None:
|
||||
"""Adding a package to a pre-release install keeps the pre-release channel.
|
||||
|
||||
The `--reinstall` rebuild re-resolves the unpinned `deepagents-code`
|
||||
requirement from scratch; without `--prerelease allow` uv would resolve
|
||||
to the latest stable and silently downgrade a pre-release user.
|
||||
"""
|
||||
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }')
|
||||
_write_dist_info(tmp_path, "deepagents-code")
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
|
||||
with patch("deepagents_code.update_check.__version__", "1.0.0a1"):
|
||||
command = install_package_command(
|
||||
"langchain-custom", distribution_name="deepagents-code"
|
||||
)
|
||||
|
||||
assert command == (
|
||||
"uv tool install --reinstall -U deepagents-code "
|
||||
"--with langchain-custom --prerelease allow"
|
||||
)
|
||||
|
||||
def test_stable_channel_omits_prerelease_flag(self, tmp_path, monkeypatch) -> None:
|
||||
"""A stable install does not add `--prerelease allow` for a package add.
|
||||
|
||||
The negative companion to `test_preserves_prerelease_channel`: pins that
|
||||
the channel is *inferred* from the installed version, so an inverted
|
||||
condition that always emitted `--prerelease allow` would fail here.
|
||||
"""
|
||||
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }')
|
||||
_write_dist_info(tmp_path, "deepagents-code")
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
|
||||
with patch("deepagents_code.update_check.__version__", "1.0.0"):
|
||||
command = install_package_command(
|
||||
"langchain-custom", distribution_name="deepagents-code"
|
||||
)
|
||||
|
||||
assert command == (
|
||||
"uv tool install --reinstall -U deepagents-code --with langchain-custom"
|
||||
)
|
||||
|
||||
def test_appends_new_with_package_after_sorted_receipt_packages(
|
||||
self, tmp_path, monkeypatch
|
||||
) -> None:
|
||||
"""A new `--with` package is appended after preserved ones, not re-sorted.
|
||||
|
||||
Preserved receipt packages come back sorted, and the new package is
|
||||
appended afterward regardless of where it would sort. Using a new name
|
||||
(`langchain-alpha`) that sorts *before* the receipt's (`langchain-zeta`)
|
||||
distinguishes this append-after-preserved contract from a plain
|
||||
alphabetical sort of the union, which the same-order cases cannot.
|
||||
"""
|
||||
_write_uv_receipt(
|
||||
tmp_path,
|
||||
'{ name = "deepagents-code" }, { name = "langchain-zeta" }',
|
||||
)
|
||||
_write_dist_info(tmp_path, "deepagents-code")
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
|
||||
command = install_package_command(
|
||||
"langchain-alpha", distribution_name="deepagents-code"
|
||||
)
|
||||
|
||||
assert command == (
|
||||
"uv tool install --reinstall -U deepagents-code "
|
||||
"--with langchain-zeta --with langchain-alpha"
|
||||
)
|
||||
|
||||
def test_unpreservable_receipt_with_requirement_raises(
|
||||
self, tmp_path, monkeypatch
|
||||
) -> None:
|
||||
"""A `--with` requirement uv can't re-express by name aborts the build.
|
||||
|
||||
Exercises the `unsupported_keys` arm through `install_package_command`'s
|
||||
newly-added receipt read: a source-pinned `--with` entry (e.g. an
|
||||
editable install) carries keys beyond `name`, so it cannot be safely
|
||||
re-expressed as a `--with <name>` and must raise rather than be silently
|
||||
dropped from the rebuilt command.
|
||||
"""
|
||||
_write_uv_receipt(
|
||||
tmp_path,
|
||||
'{ name = "deepagents-code" }, '
|
||||
'{ name = "langchain-custom", editable = true }',
|
||||
)
|
||||
_write_dist_info(tmp_path, "deepagents-code")
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
monkeypatch.syspath_prepend(str(tmp_path))
|
||||
|
||||
with pytest.raises(
|
||||
ToolRequirementIntrospectionError,
|
||||
match="cannot be preserved automatically",
|
||||
):
|
||||
install_package_command(
|
||||
"langchain-new", distribution_name="deepagents-code"
|
||||
)
|
||||
|
||||
def test_refuses_missing_distribution(self) -> None:
|
||||
"""Reinstalls must not drop extras when metadata is unavailable."""
|
||||
with pytest.raises(ExtrasIntrospectionError, match="cannot preserve"):
|
||||
@@ -2968,7 +3124,8 @@ class TestIsValidPackageName:
|
||||
def test_rejects_option_injection_leading_dash(self) -> None:
|
||||
"""A leading dash would smuggle uv options into `--with <name>`.
|
||||
|
||||
The command is `uv tool install -U deepagents-code --with <name>`; a name
|
||||
The command is `uv tool install --reinstall -U deepagents-code --with
|
||||
<name>`; a name
|
||||
like `-rreqs.txt` or `--editable` would be parsed by uv as a flag, not a
|
||||
package. The validator must reject these regardless of `--force`/`--yes`.
|
||||
"""
|
||||
@@ -3118,6 +3275,40 @@ class TestPerformInstallPackage:
|
||||
assert "metadata unreadable" in output
|
||||
assert "introspect installed extras" in caplog.text
|
||||
|
||||
async def test_uv_receipt_failure_is_reported_and_logged(
|
||||
self, tmp_path, monkeypatch, caplog
|
||||
) -> None:
|
||||
"""An unreadable uv receipt surfaces as a reported, logged error.
|
||||
|
||||
`install_package_command` now reads the uv tool receipt to preserve the
|
||||
interpreter and `--with` packages, so a malformed receipt raises
|
||||
`ToolRequirementIntrospectionError`. The executor must report it rather
|
||||
than let it escape unhandled — narrowing back to `except
|
||||
ExtrasIntrospectionError` would let the error crash the caller.
|
||||
"""
|
||||
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }, "bad"')
|
||||
monkeypatch.setattr("sys.prefix", str(tmp_path))
|
||||
with (
|
||||
patch(
|
||||
"deepagents_code.update_check.detect_install_method",
|
||||
return_value="uv",
|
||||
),
|
||||
patch(
|
||||
"deepagents_code.update_check.shutil.which",
|
||||
return_value="/usr/bin/uv",
|
||||
),
|
||||
patch(
|
||||
"deepagents_code.extras_info.installed_extra_names",
|
||||
return_value=frozenset(),
|
||||
),
|
||||
caplog.at_level(logging.WARNING, logger="deepagents_code.update_check"),
|
||||
):
|
||||
success, output = await perform_install_package("langchain-custom")
|
||||
assert success is False
|
||||
assert "ToolRequirementIntrospectionError" in output
|
||||
assert "non-table requirement" in output
|
||||
assert "uv receipt" in caplog.text
|
||||
|
||||
|
||||
class TestRunInstallSubprocessFailureModes:
|
||||
"""Failure-mode coverage routed through `perform_install_extra`.
|
||||
@@ -3166,7 +3357,9 @@ class TestRunInstallSubprocessFailureModes:
|
||||
),
|
||||
patch(
|
||||
"deepagents_code.update_check._install_extra_uv_tool_command",
|
||||
return_value="uv tool install -U 'deepagents-code[quickjs]'",
|
||||
return_value=(
|
||||
"uv tool install --reinstall -U 'deepagents-code[quickjs]'"
|
||||
),
|
||||
),
|
||||
patch("asyncio.create_subprocess_shell", side_effect=_raise),
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user