Skip to content

Commit 08ca5aa

Browse files
fix(connectors): actionable error when keyring missing on base install (#1622)
A bare `pip install amd-gaia` (no extras) ships `gaia connectors` as a base command, but `keyring` lives only in the `[ui]`/`[api]`/`[dev]` extras — so `gaia connectors list`/`status` and the provider-credential paths crashed with a raw `ModuleNotFoundError: No module named 'keyring'` that named neither the cause nor the fix. Now those subcommands fail loudly with an actionable `ConnectorsError` — `gaia connectors needs the 'keyring' package … pip install keyring (or pip install "amd-gaia[ui]")` — which the CLI prints to stderr instead of a traceback. This is the base-CLI half of the gap #1617/#1620 fixed for the `[api]` partner recipe. It implements the issue's Option 3 (guarded import) — the part the issue itself calls "worth doing regardless." The larger base-dep-vs-`[connectors]`-extra packaging decision (Options 1/2) is **left to a maintainer** (flagged for @kovtcharov-amd in the issue); this guard holds regardless of where `keyring` ultimately lands, and on its own satisfies the issue's acceptance branch "*fails with an actionable error naming the exact install command.*" Refs #1621 ## Test plan - [ ] `python -m pytest tests/unit/connectors/test_keyring_guard.py -x` passes (new regression guard: import without `keyring` → `ConnectorsError` naming `pip install`; with `keyring` → real module re-exported) - [ ] `python -m pytest tests/unit/connectors/ -x` passes (no regression in store/mcp_server keyring usage) - [ ] `python util/lint.py --all` passes - [ ] Manual: in a venv with **no** `keyring` installed, `gaia connectors list` prints `Connectors error: gaia connectors needs the 'keyring' package … pip install keyring …` and exits non-zero (instead of a `ModuleNotFoundError` traceback) Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Tomasz Iniewicz <itomek@users.noreply.github.com>
1 parent 881ebcf commit 08ca5aa

4 files changed

Lines changed: 106 additions & 5 deletions

File tree

src/gaia/connectors/_keyring.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2+
# SPDX-License-Identifier: MIT
3+
"""
4+
Guarded import of the optional ``keyring`` dependency (#1621).
5+
6+
``gaia connectors`` is a *base* CLI command, but ``keyring`` ships only in the
7+
``[ui]``/``[api]``/``[dev]`` extras — never in base ``install_requires``. A bare
8+
``pip install amd-gaia`` therefore reaches ``import keyring`` deep in the store
9+
on the list/status/credential subcommands and dies with a raw
10+
``ModuleNotFoundError`` that names neither the cause nor the fix.
11+
12+
Importing ``keyring`` through this module turns that into the actionable error
13+
CLAUDE.md's "fail loudly" rule requires: it names what failed, what to install,
14+
and where to read more. The re-exported name is the real ``keyring`` module
15+
(with ``keyring.errors`` pre-imported), so callers use it exactly as before.
16+
"""
17+
18+
from __future__ import annotations
19+
20+
from gaia.connectors.errors import ConnectorsError
21+
22+
try:
23+
import keyring
24+
import keyring.errors # noqa: F401 # re-exported as ``keyring.errors``
25+
except ImportError as e: # pragma: no cover - exercised via reload in tests
26+
raise ConnectorsError(
27+
"gaia connectors needs the 'keyring' package, which is not installed. "
28+
'Install it with `pip install keyring` (or `pip install "amd-gaia[ui]"` '
29+
"for the full Agent UI install). "
30+
"See docs/sdk/infrastructure/connections.mdx."
31+
) from e
32+
33+
__all__ = ["keyring"]

src/gaia/connectors/mcp_server.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@
2727
from pathlib import Path
2828
from typing import Any, Callable, Dict, List, Optional
2929

30-
import keyring
31-
30+
from gaia.connectors._keyring import keyring # actionable error if missing (#1621)
3231
from gaia.connectors.errors import ConnectorsError
3332
from gaia.connectors.handler import register_handler
3433
from gaia.connectors.spec import ConnectorSpec

src/gaia/connectors/store.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@
3535
import time
3636
from typing import List, Optional
3737

38-
import keyring
39-
import keyring.errors
40-
38+
from gaia.connectors._keyring import keyring # actionable error if missing (#1621)
4139
from gaia.connectors.errors import (
4240
AuthRequiredError,
4341
ConnectorsError,
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2+
# SPDX-License-Identifier: MIT
3+
"""Regression test for issue #1621.
4+
5+
``gaia connectors`` is a *base* CLI command, but ``keyring`` ships only in the
6+
``[ui]``/``[api]``/``[dev]`` extras. A bare ``pip install amd-gaia`` reaches
7+
``import keyring`` deep in ``gaia.connectors.store`` on the list/status/credential
8+
subcommands and used to die with a raw ``ModuleNotFoundError`` that named neither
9+
the cause nor the fix.
10+
11+
``gaia.connectors._keyring`` guards that import and re-raises a ``ConnectorsError``
12+
naming the exact install command — CLAUDE.md's "fail loudly / actionable errors"
13+
rule. The CLI handler (``gaia.connectors.cli.handle``) catches ``ConnectorsError``
14+
and prints it to stderr, so the user sees the install hint instead of a traceback.
15+
16+
The promotion of ``keyring`` to a base dep vs. a dedicated extra is a separate
17+
maintainer packaging decision; this guard is the defense-in-depth half that holds
18+
regardless of where the dependency ultimately lives.
19+
"""
20+
21+
from __future__ import annotations
22+
23+
import builtins
24+
import importlib
25+
import sys
26+
27+
import pytest
28+
29+
from gaia.connectors.errors import ConnectorsError
30+
31+
32+
def test_keyring_guard_raises_actionable_error(monkeypatch: pytest.MonkeyPatch) -> None:
33+
"""Importing the keyring shim without ``keyring`` installed must raise an
34+
actionable ``ConnectorsError`` (not a bare ``ModuleNotFoundError``)."""
35+
real_import = builtins.__import__
36+
37+
def _blocked_import(name, *args, **kwargs):
38+
if name == "keyring" or name.startswith("keyring."):
39+
raise ModuleNotFoundError("No module named 'keyring'")
40+
return real_import(name, *args, **kwargs)
41+
42+
# Drop any cached keyring + shim modules so the guarded import re-runs.
43+
for mod in list(sys.modules):
44+
if (
45+
mod == "keyring"
46+
or mod.startswith("keyring.")
47+
or mod == "gaia.connectors._keyring"
48+
):
49+
monkeypatch.delitem(sys.modules, mod, raising=False)
50+
51+
monkeypatch.setattr(builtins, "__import__", _blocked_import)
52+
53+
with pytest.raises(ConnectorsError) as excinfo:
54+
importlib.import_module("gaia.connectors._keyring")
55+
56+
msg = str(excinfo.value)
57+
assert "keyring" in msg
58+
assert "pip install" in msg
59+
60+
# Restore a clean shim for the rest of the suite (keyring is available again
61+
# now that the monkeypatch is being torn down at function exit).
62+
sys.modules.pop("gaia.connectors._keyring", None)
63+
64+
65+
def test_keyring_shim_reexports_real_module() -> None:
66+
"""When ``keyring`` IS installed the shim re-exports the real module,
67+
so ``store``/``mcp_server`` keep using ``keyring`` / ``keyring.errors``."""
68+
shim = importlib.import_module("gaia.connectors._keyring")
69+
real = importlib.import_module("keyring")
70+
assert shim.keyring is real
71+
assert hasattr(shim.keyring, "errors")

0 commit comments

Comments
 (0)