diff --git a/libs/code/deepagents_code/extras_info.py b/libs/code/deepagents_code/extras_info.py index 8a94326d27d..b38e0501a32 100644 --- a/libs/code/deepagents_code/extras_info.py +++ b/libs/code/deepagents_code/extras_info.py @@ -1085,11 +1085,7 @@ def format_known_extras() -> str: ExtrasStatus = dict[str, list[tuple[str, str]]] -"""Mapping from extra name to `(package, installed_version)` tuples. - -Only packages that are actually installed are included. Extras whose -declared packages are all missing are omitted entirely. -""" +"""Mapping from ready extra names to `(package, installed_version)` tuples.""" @dataclass(frozen=True) @@ -1128,13 +1124,13 @@ def _extract_extra_name(marker_str: str) -> str | None: def get_extras_status( distribution_name: str = "deepagents-code", ) -> ExtrasStatus: - """Return installed optional dependencies grouped by extra. + """Return ready optional dependencies grouped by extra. Reads `Requires-Dist` metadata from the named distribution, groups the entries gated by `extra == "..."` markers under their extra name, and resolves each package's installed version via `importlib.metadata`. - Packages that are not installed are omitted; extras whose entire - package list is absent are dropped. + Partially installed extras are omitted so shared transitive dependencies + do not make unavailable integrations appear installed. Composite meta-extras that only bundle other extras (see `_COMPOSITE_EXTRAS`) and self-references to the distribution itself @@ -1144,13 +1140,12 @@ def get_extras_status( distribution_name: Name of the installed distribution to inspect. Returns: - Mapping from extra name to a sorted list of `(package, version)` - tuples for packages that are currently installed. An empty - mapping is returned when the distribution itself is not found. + Mapping from ready extra names to sorted `(package, version)` tuples. An + empty mapping is returned when the distribution itself is not found. """ result: ExtrasStatus = {} for extra in get_optional_dependency_status(distribution_name): - if extra.installed: + if extra.ready: result[extra.name] = list(extra.installed) return result @@ -1162,6 +1157,11 @@ def installed_extra_names( ) -> set[str]: """Return extras with at least one installed dependency. + Deliberately looser than `get_extras_status`, which requires every declared + package to be present. Callers here rebuild install commands, so a partially + installed extra must still be preserved across an upgrade; dropping it would + silently uninstall what the user asked for. Do not harmonize the two. + Args: distribution_name: Name of the installed distribution to inspect. strict: Raise when the distribution metadata cannot be read or parsed diff --git a/libs/code/tests/unit_tests/test_extras_info.py b/libs/code/tests/unit_tests/test_extras_info.py index 4099059b406..dc6dc095e5e 100644 --- a/libs/code/tests/unit_tests/test_extras_info.py +++ b/libs/code/tests/unit_tests/test_extras_info.py @@ -130,20 +130,43 @@ def test_real_distribution_skips_self_references() -> None: assert pkg_name.lower() != "deepagents-code" -def test_missing_packages_are_omitted() -> None: +def test_incomplete_extras_are_omitted() -> None: mock_dist = MagicMock() mock_dist.requires = [ "langchain-anthropic>=1.0.0 ; extra == 'anthropic'", "fake-absent-package>=1.0.0 ; extra == 'custom'", - "partially-present>=1.0.0 ; extra == 'mixed'", - "also-missing>=1.0.0 ; extra == 'mixed'", + "aiohttp>=3.14.3 ; extra == 'nvidia'", + "langchain-nvidia-ai-endpoints>=1.4.3 ; extra == 'nvidia'", ] def fake_version(name: str) -> str: if name == "langchain-anthropic": return "1.4.0" - if name == "partially-present": - return "2.0.0" + if name == "aiohttp": + return "3.14.3" + raise PackageNotFoundError(name) + + with ( + patch("deepagents_code.extras_info.distribution", return_value=mock_dist), + patch("deepagents_code.extras_info.pkg_version", side_effect=fake_version), + ): + extras = get_extras_status() + + assert extras == {"anthropic": [("langchain-anthropic", "1.4.0")]} + + +def test_complete_multi_package_extras_are_kept() -> None: + mock_dist = MagicMock() + mock_dist.requires = [ + "aiohttp>=3.14.3 ; extra == 'nvidia'", + "langchain-nvidia-ai-endpoints>=1.4.3 ; extra == 'nvidia'", + ] + + def fake_version(name: str) -> str: + if name == "aiohttp": + return "3.14.3" + if name == "langchain-nvidia-ai-endpoints": + return "1.4.3" raise PackageNotFoundError(name) with ( @@ -152,11 +175,11 @@ def fake_version(name: str) -> str: ): extras = get_extras_status() - # Fully absent extras disappear; partially present extras keep only - # the installed packages. assert extras == { - "anthropic": [("langchain-anthropic", "1.4.0")], - "mixed": [("partially-present", "2.0.0")], + "nvidia": [ + ("aiohttp", "3.14.3"), + ("langchain-nvidia-ai-endpoints", "1.4.3"), + ], }