fix(code): pin app version when installing extras (#4313)

Installing a provider extra rebuilds the `dcode` uv tool environment.
That rebuild should keep the currently running app version, not let uv
backtrack to an older `deepagents-code` release when the latest stable
app depends on prerelease packages.

## Changes
- Pin `_install_extra_uv_tool_command` and `install_package_command` to
the running `deepagents-code` version and pass `--prerelease allow` for
dependency resolution.
- Keep malformed app version pins as `ValueError` while preserving the
existing `ExtrasIntrospectionError` path for invalid installed-extra
metadata.
- Update command-builder and executor coverage for pinned extra/package
installs, prerelease dependency resolution, and invalid version
handling.
This commit is contained in:
Mason Daugherty
2026-06-26 03:28:53 -04:00
committed by GitHub
parent 38a370b829
commit c20c8e2fc1
4 changed files with 113 additions and 85 deletions
+26 -20
View File
@@ -2123,6 +2123,7 @@ def _uv_tool_install_command(
Raises:
ExtrasIntrospectionError: If a metadata-sourced extra name fails PEP 508
validation.
ValueError: If `version` is not PEP 440 compliant.
Propagates `ToolRequirementIntrospectionError` if the uv tool receipt's
interpreter or `--with` packages cannot be determined safely from the tool
@@ -2138,6 +2139,8 @@ def _uv_tool_install_command(
try:
requirement = _dcode_extras_requirement(extras, version=version)
except ValueError as exc:
if str(exc).startswith("Invalid deepagents-code version"):
raise
msg = f"Distribution metadata yielded an invalid extra name: {exc}"
raise ExtrasIntrospectionError(msg) from exc
cmd = "uv tool install --reinstall -U" if reinstall else "uv tool install -U"
@@ -2298,13 +2301,13 @@ def install_package_command(
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.
already-installed extras into the pinned `deepagents-code[...]` requirement,
and preserves the uv-managed Python interpreter and the receipt's existing
`--with` packages. 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 when the latest stable app depends on prerelease packages.
Like the extras path (`_install_extra_uv_tool_command`), passes
`reinstall=True` so the upgrade rebuilds the tool environment cleanly; see
@@ -2335,8 +2338,8 @@ def install_package_command(
)
raise ValueError(msg)
return _uv_tool_install_command(
version=None,
include_prereleases=None,
version=__version__,
include_prereleases=True,
distribution_name=distribution_name,
with_packages_to_add=(package,),
reinstall=True,
@@ -2434,6 +2437,8 @@ def _install_extra_uv_tool_command(
) -> str:
"""Return the receipt-preserving uv command that installs one dcode extra.
Pins the running `deepagents-code` version and allows prerelease dependency
resolution so adding an extra cannot make uv backtrack to an older app release.
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.
@@ -2459,8 +2464,8 @@ def _install_extra_uv_tool_command(
)
raise ValueError(msg)
return _uv_tool_install_command(
version=None,
include_prereleases=None,
version=__version__,
include_prereleases=True,
distribution_name=distribution_name,
extras_to_add=(extra,),
reinstall=True,
@@ -2504,11 +2509,11 @@ async def perform_install_extra(
) -> tuple[bool, str]:
"""Add `extra` to the installed dcode tool environment.
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.
Runs `uv tool install --reinstall -U 'deepagents-code[<extras>]==<current>'
--prerelease allow`, 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`;
@@ -2575,10 +2580,11 @@ async def perform_install_package(
) -> tuple[bool, str]:
"""Add an arbitrary `package` to the installed dcode tool environment.
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.
Runs `uv tool install --reinstall -U 'deepagents-code[<extras>]==<current>'
--with <package> --prerelease allow`, 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.
+4 -3
View File
@@ -33,7 +33,7 @@ from textual.screen import ModalScreen
from textual.widget import Widget
from textual.widgets import Checkbox, Input, Static
from deepagents_code._version import CHANGELOG_URL
from deepagents_code._version import CHANGELOG_URL, __version__
from deepagents_code.app import (
_DEEPAGENTS_IMPORT_LOCK,
_TYPING_IDLE_THRESHOLD_SECONDS,
@@ -9161,8 +9161,9 @@ class TestDeferredActions:
assert isinstance(widget, ErrorMessage)
rendered = str(widget._content)
assert (
"uv tool install --reinstall -U deepagents-code "
"--with langchain-custom_provider" in rendered
"uv tool install --reinstall -U "
f"deepagents-code=={__version__} "
"--with langchain-custom_provider --prerelease allow" in rendered
)
assert "/model custom_provider:<model>" in rendered
+4 -2
View File
@@ -12,6 +12,7 @@ import pytest
from deepagents_code import _git as git_module, model_config
from deepagents_code._env_vars import SERVER_ENV_PREFIX
from deepagents_code._version import __version__
from deepagents_code.config import (
CLI_MAX_RETRIES_KEY,
RECOMMENDED_SAFE_SHELL_COMMANDS,
@@ -3929,8 +3930,9 @@ class TestCreateModelViaInitImportError:
pytest.raises(
ModelConfigError,
match=(
"Install with: uv tool install --reinstall -U deepagents-code "
"--with langchain-custom_provider"
"Install with: uv tool install --reinstall -U "
f"deepagents-code=={__version__} "
"--with langchain-custom_provider --prerelease allow"
),
),
):
+79 -60
View File
@@ -2848,7 +2848,8 @@ class TestInstallExtraCommand:
assert install_extra_recovery_command("quickjs") == (
"uv tool install --reinstall -U --python '/opt/Python 3.13/bin/python' "
"'deepagents-code[nvidia,quickjs]' --with langchain-custom"
f"'deepagents-code[nvidia,quickjs]=={__version__}' "
"--with langchain-custom --prerelease allow"
)
def test_recovery_command_uses_script_for_non_uv_without_receipt(
@@ -2903,11 +2904,11 @@ class TestInstallExtraCommand:
monkeypatch.syspath_prepend(str(tmp_path))
assert installed_extra_names("deepagents-code") == {"nvidia"}
assert (
_install_extra_uv_tool_command(
"baseten", distribution_name="deepagents-code"
)
== "uv tool install --reinstall -U 'deepagents-code[baseten,nvidia]'"
assert _install_extra_uv_tool_command(
"baseten", distribution_name="deepagents-code"
) == (
"uv tool install --reinstall -U "
f"'deepagents-code[baseten,nvidia]=={__version__}' --prerelease allow"
)
def test_uv_install_extra_command_dedupes_existing_extra(
@@ -2924,11 +2925,11 @@ class TestInstallExtraCommand:
monkeypatch.setattr("sys.prefix", str(tmp_path))
monkeypatch.syspath_prepend(str(tmp_path))
assert (
_install_extra_uv_tool_command(
"nvidia", distribution_name="deepagents-code"
)
== "uv tool install --reinstall -U 'deepagents-code[nvidia]'"
assert _install_extra_uv_tool_command(
"nvidia", distribution_name="deepagents-code"
) == (
"uv tool install --reinstall -U "
f"'deepagents-code[nvidia]=={__version__}' --prerelease allow"
)
def test_uv_install_extra_command_drops_composite_extras(
@@ -2950,11 +2951,11 @@ class TestInstallExtraCommand:
monkeypatch.syspath_prepend(str(tmp_path))
assert installed_extra_names("deepagents-code") == {"nvidia"}
assert (
_install_extra_uv_tool_command(
"baseten", distribution_name="deepagents-code"
)
== "uv tool install --reinstall -U 'deepagents-code[baseten,nvidia]'"
assert _install_extra_uv_tool_command(
"baseten", distribution_name="deepagents-code"
) == (
"uv tool install --reinstall -U "
f"'deepagents-code[baseten,nvidia]=={__version__}' --prerelease allow"
)
def test_uv_install_extra_command_preserves_receipt_python_and_with_packages(
@@ -2981,7 +2982,8 @@ class TestInstallExtraCommand:
assert command == (
"uv tool install --reinstall -U --python '/opt/Python 3.13/bin/python' "
"'deepagents-code[baseten,nvidia]' --with langchain-custom"
f"'deepagents-code[baseten,nvidia]=={__version__}' "
"--with langchain-custom --prerelease allow"
)
def test_sorts_extras_deterministically(self) -> None:
@@ -3018,7 +3020,7 @@ class TestInstallPackageCommand:
"""`install_package_command` builds a uv tool package install string."""
def test_basic_no_extras(self, tmp_path, monkeypatch) -> None:
"""Clean metadata with no installed extras yields a plain requirement."""
"""Clean metadata with no extras yields the version-pinned requirement."""
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }')
_write_dist_info(
tmp_path,
@@ -3028,11 +3030,12 @@ class TestInstallPackageCommand:
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 --reinstall -U deepagents-code --with langchain-custom"
assert install_package_command(
"langchain-custom", distribution_name="deepagents-code"
) == (
"uv tool install --reinstall -U "
f"deepagents-code=={__version__} --with langchain-custom "
"--prerelease allow"
)
def test_allows_pep508_name_separators(self, tmp_path, monkeypatch) -> None:
@@ -3045,12 +3048,12 @@ class TestInstallPackageCommand:
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 --reinstall -U deepagents-code "
"--with langchain.custom_provider"
assert install_package_command(
"langchain.custom_provider", distribution_name="deepagents-code"
) == (
"uv tool install --reinstall -U "
f"deepagents-code=={__version__} --with langchain.custom_provider "
"--prerelease allow"
)
def test_preserves_installed_extras(self, tmp_path, monkeypatch) -> None:
@@ -3069,12 +3072,12 @@ class TestInstallPackageCommand:
monkeypatch.syspath_prepend(str(tmp_path))
assert installed_extra_names("deepagents-code") == {"nvidia"}
assert (
install_package_command(
"langchain-custom", distribution_name="deepagents-code"
)
== "uv tool install --reinstall -U 'deepagents-code[nvidia]' "
"--with langchain-custom"
assert install_package_command(
"langchain-custom", distribution_name="deepagents-code"
) == (
"uv tool install --reinstall -U "
f"'deepagents-code[nvidia]=={__version__}' --with langchain-custom "
"--prerelease allow"
)
def test_preserves_receipt_python_and_with_packages(
@@ -3101,8 +3104,8 @@ class TestInstallPackageCommand:
assert command == (
"uv tool install --reinstall -U --python '/opt/Python 3.13/bin/python' "
"'deepagents-code[nvidia]' --with langchain-first "
"--with langchain-second"
f"'deepagents-code[nvidia]=={__version__}' --with langchain-first "
"--with langchain-second --prerelease allow"
)
def test_does_not_duplicate_existing_receipt_package(
@@ -3121,18 +3124,14 @@ class TestInstallPackageCommand:
"LangChain_Custom", distribution_name="deepagents-code"
)
assert (
command
== "uv tool install --reinstall -U deepagents-code --with langchain-custom"
assert command == (
"uv tool install --reinstall -U "
f"deepagents-code=={__version__} --with langchain-custom "
"--prerelease allow"
)
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.
"""
def test_pins_prerelease_app_version(self, tmp_path, monkeypatch) -> None:
"""Adding a package to a pre-release install keeps that exact app version."""
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }')
_write_dist_info(tmp_path, "deepagents-code")
monkeypatch.setattr("sys.prefix", str(tmp_path))
@@ -3144,17 +3143,14 @@ class TestInstallPackageCommand:
)
assert command == (
"uv tool install --reinstall -U deepagents-code "
"uv tool install --reinstall -U deepagents-code==1.0.0a1 "
"--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.
"""
def test_stable_install_pins_app_and_allows_prerelease_deps(
self, tmp_path, monkeypatch
) -> None:
"""A stable app reinstall keeps the app pinned while allowing rc deps."""
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }')
_write_dist_info(tmp_path, "deepagents-code")
monkeypatch.setattr("sys.prefix", str(tmp_path))
@@ -3166,7 +3162,8 @@ class TestInstallPackageCommand:
)
assert command == (
"uv tool install --reinstall -U deepagents-code --with langchain-custom"
"uv tool install --reinstall -U deepagents-code==1.0.0 "
"--with langchain-custom --prerelease allow"
)
def test_appends_new_with_package_after_sorted_receipt_packages(
@@ -3193,8 +3190,9 @@ class TestInstallPackageCommand:
)
assert command == (
"uv tool install --reinstall -U deepagents-code "
"--with langchain-zeta --with langchain-alpha"
"uv tool install --reinstall -U "
f"deepagents-code=={__version__} --with langchain-zeta "
"--with langchain-alpha --prerelease allow"
)
def test_unpreservable_receipt_with_requirement_raises(
@@ -3413,8 +3411,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 --reinstall -U deepagents-code --with
<name>`; a name
The command is `uv tool install --reinstall -U deepagents-code==<version>
--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`.
"""
@@ -3598,6 +3596,27 @@ class TestPerformInstallPackage:
assert "non-table requirement" in output
assert "uv receipt" in caplog.text
async def test_invalid_app_version_is_reported(self, tmp_path, monkeypatch) -> None:
"""A malformed app version pin is reported instead of escaping."""
_write_uv_receipt(tmp_path, '{ name = "deepagents-code" }')
_write_dist_info(tmp_path, "deepagents-code")
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.update_check.__version__", "not-a-version"),
):
success, output = await perform_install_package("langchain-custom")
assert success is False
assert "ValueError" in output
assert "Invalid deepagents-code version" in output
class TestRunInstallSubprocessFailureModes:
"""Failure-mode coverage routed through `perform_install_extra`.