fix: handle missing keyring backend in CI
CI / Tests (macOS) (push) Has been cancelled
CI / Tests (Windows) (push) Has been cancelled
CI / Tests (Linux, Python 3.11) (push) Has been cancelled
CI / Tests (Linux, Python 3.12) (push) Has been cancelled
CI / Tests (Linux, Python 3.9) (push) Has been cancelled
CI / Tests (macOS) (push) Has been cancelled
CI / Tests (Windows) (push) Has been cancelled
CI / Tests (Linux, Python 3.11) (push) Has been cancelled
CI / Tests (Linux, Python 3.12) (push) Has been cancelled
CI / Tests (Linux, Python 3.9) (push) Has been cancelled
The keyring package imports successfully on headless Linux but uses a fail.Keyring backend that raises NoKeyringError on any operation. Detect this case in _get_keyring() and fall back to the file-based token store.
This commit is contained in:
+35
-15
@@ -49,20 +49,37 @@ class Settings:
|
||||
# import fails for some reason.
|
||||
|
||||
|
||||
def _keyring_available() -> bool:
|
||||
def _get_keyring():
|
||||
"""Return the keyring module if a usable backend is available, else None.
|
||||
|
||||
Importing `keyring` can succeed even on systems where no backend is
|
||||
installed (e.g., headless Linux CI runners). In that case the backend is
|
||||
`keyring.backends.fail.Keyring`, which raises `NoKeyringError` on any
|
||||
operation. Detect that case up front and fall back to the file store.
|
||||
"""
|
||||
try:
|
||||
import keyring # noqa: F401
|
||||
return True
|
||||
import keyring
|
||||
from keyring.backends import fail
|
||||
except ImportError:
|
||||
return False
|
||||
return None
|
||||
try:
|
||||
backend = keyring.get_keyring()
|
||||
except Exception:
|
||||
return None
|
||||
if isinstance(backend, fail.Keyring):
|
||||
return None
|
||||
return keyring
|
||||
|
||||
|
||||
def save_token(token: str) -> str:
|
||||
"""Persist token. Returns a human-readable location description."""
|
||||
if _keyring_available():
|
||||
import keyring
|
||||
keyring.set_password(KEYRING_SERVICE, KEYRING_USER, token)
|
||||
return "system keyring"
|
||||
keyring = _get_keyring()
|
||||
if keyring is not None:
|
||||
try:
|
||||
keyring.set_password(KEYRING_SERVICE, KEYRING_USER, token)
|
||||
return "system keyring"
|
||||
except Exception:
|
||||
pass
|
||||
TOKEN_FALLBACK_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
TOKEN_FALLBACK_FILE.write_text(token)
|
||||
try:
|
||||
@@ -73,19 +90,22 @@ def save_token(token: str) -> str:
|
||||
|
||||
|
||||
def load_token() -> str | None:
|
||||
if _keyring_available():
|
||||
import keyring
|
||||
tok = keyring.get_password(KEYRING_SERVICE, KEYRING_USER)
|
||||
if tok:
|
||||
return tok
|
||||
keyring = _get_keyring()
|
||||
if keyring is not None:
|
||||
try:
|
||||
tok = keyring.get_password(KEYRING_SERVICE, KEYRING_USER)
|
||||
if tok:
|
||||
return tok
|
||||
except Exception:
|
||||
pass
|
||||
if TOKEN_FALLBACK_FILE.exists():
|
||||
return TOKEN_FALLBACK_FILE.read_text().strip() or None
|
||||
return None
|
||||
|
||||
|
||||
def clear_token() -> None:
|
||||
if _keyring_available():
|
||||
import keyring
|
||||
keyring = _get_keyring()
|
||||
if keyring is not None:
|
||||
try:
|
||||
keyring.delete_password(KEYRING_SERVICE, KEYRING_USER)
|
||||
except Exception:
|
||||
|
||||
@@ -64,7 +64,7 @@ def test_settings_load_ignores_unknown_keys(config_module, tmp_path):
|
||||
|
||||
def test_token_fallback_roundtrip(config_module, monkeypatch):
|
||||
"""Force the non-keyring fallback path and verify round-trip."""
|
||||
monkeypatch.setattr(config_module, "_keyring_available", lambda: False)
|
||||
monkeypatch.setattr(config_module, "_get_keyring", lambda: None)
|
||||
|
||||
assert config_module.load_token() is None
|
||||
|
||||
@@ -82,7 +82,7 @@ def test_token_fallback_file_permissions(config_module, monkeypatch):
|
||||
import os
|
||||
import stat
|
||||
|
||||
monkeypatch.setattr(config_module, "_keyring_available", lambda: False)
|
||||
monkeypatch.setattr(config_module, "_get_keyring", lambda: None)
|
||||
config_module.save_token("sl.perm-test")
|
||||
|
||||
mode = os.stat(config_module.TOKEN_FALLBACK_FILE).st_mode
|
||||
@@ -96,8 +96,8 @@ def test_token_keyring_roundtrip(config_module):
|
||||
"""If keyring is installed, verify we actually use it."""
|
||||
pytest.importorskip("keyring")
|
||||
|
||||
if not config_module._keyring_available():
|
||||
pytest.skip("keyring not importable")
|
||||
if config_module._get_keyring() is None:
|
||||
pytest.skip("keyring not usable")
|
||||
|
||||
import keyring
|
||||
# Use the in-memory fallback backend to avoid touching real Keychain.
|
||||
|
||||
Reference in New Issue
Block a user