diff --git a/src/pkgmgr/core/credentials/__init__.py b/src/pkgmgr/core/credentials/__init__.py index 28bb283..7820ba6 100644 --- a/src/pkgmgr/core/credentials/__init__.py +++ b/src/pkgmgr/core/credentials/__init__.py @@ -3,6 +3,7 @@ from .resolver import ResolutionOptions, TokenResolver from .types import ( CredentialError, + KeyringOperationError, KeyringUnavailableError, NoCredentialsError, TokenRequest, @@ -11,6 +12,7 @@ from .types import ( __all__ = [ "CredentialError", + "KeyringOperationError", "KeyringUnavailableError", "NoCredentialsError", "ResolutionOptions", diff --git a/src/pkgmgr/core/credentials/providers/keyring.py b/src/pkgmgr/core/credentials/providers/keyring.py index 9b7a1ed..88f82e0 100644 --- a/src/pkgmgr/core/credentials/providers/keyring.py +++ b/src/pkgmgr/core/credentials/providers/keyring.py @@ -2,10 +2,14 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Optional from ..store_keys import build_keyring_key -from ..types import KeyringUnavailableError, TokenRequest, TokenResult +from ..types import ( + KeyringOperationError, + KeyringUnavailableError, + TokenRequest, + TokenResult, +) def _import_keyring(): @@ -41,10 +45,15 @@ class KeyringTokenProvider: source_name: str = "keyring" - def get(self, request: TokenRequest) -> Optional[TokenResult]: + def get(self, request: TokenRequest) -> TokenResult | None: keyring = _import_keyring() key = build_keyring_key(request.provider_kind, request.host, request.owner) - token = keyring.get_password(key.service, key.username) + try: + token = keyring.get_password(key.service, key.username) + except Exception as exc: + raise KeyringOperationError( + f"Reading the keyring entry for {key.service!r} failed." + ) from exc if token: return TokenResult(token=token.strip(), source=self.source_name) return None @@ -52,4 +61,9 @@ class KeyringTokenProvider: def set(self, request: TokenRequest, token: str) -> None: keyring = _import_keyring() key = build_keyring_key(request.provider_kind, request.host, request.owner) - keyring.set_password(key.service, key.username, token) + try: + keyring.set_password(key.service, key.username, token) + except Exception as exc: + raise KeyringOperationError( + f"Writing the keyring entry for {key.service!r} failed." + ) from exc diff --git a/src/pkgmgr/core/credentials/resolver.py b/src/pkgmgr/core/credentials/resolver.py index c9eaea2..2499868 100644 --- a/src/pkgmgr/core/credentials/resolver.py +++ b/src/pkgmgr/core/credentials/resolver.py @@ -3,13 +3,13 @@ from __future__ import annotations import sys from dataclasses import dataclass -from typing import Optional from .providers.env import EnvTokenProvider from .providers.gh import GhTokenProvider from .providers.keyring import KeyringTokenProvider from .providers.prompt import PromptTokenProvider from .types import ( + KeyringOperationError, KeyringUnavailableError, NoCredentialsError, TokenRequest, @@ -76,7 +76,7 @@ class TokenResolver: self, request: TokenRequest, opts: ResolutionOptions, - ) -> Optional[TokenResult]: + ) -> TokenResult | None: """ Prompt for a token and optionally store it in keyring. If keyring is unavailable, still return the token for this run. @@ -93,8 +93,8 @@ class TokenResolver: self._keyring.set(request, prompt_res.token) # overwrite is fine except KeyringUnavailableError as exc: self._warn_keyring_unavailable(exc) - except Exception: - # If keyring cannot store, still use token for this run. + except KeyringOperationError: + # Storing failed; the token is still valid for this run. pass return prompt_res @@ -103,8 +103,8 @@ class TokenResolver: self, provider_kind: str, host: str, - owner: Optional[str] = None, - options: Optional[ResolutionOptions] = None, + owner: str | None = None, + options: ResolutionOptions | None = None, ) -> TokenResult: opts = options or ResolutionOptions() request = TokenRequest(provider_kind=provider_kind, host=host, owner=owner) @@ -135,8 +135,8 @@ class TokenResolver: except KeyringUnavailableError as exc: # Show a helpful warning once, then continue (prompt fallback). self._warn_keyring_unavailable(exc) - except Exception: - # Unknown keyring errors: do not block prompting; still avoid hard crash. + except KeyringOperationError: + # Reading failed; fall through to the prompt. pass # 3) Prompt (optional) diff --git a/src/pkgmgr/core/credentials/types.py b/src/pkgmgr/core/credentials/types.py index 9854399..bbe5d1d 100644 --- a/src/pkgmgr/core/credentials/types.py +++ b/src/pkgmgr/core/credentials/types.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Optional class CredentialError(RuntimeError): @@ -17,13 +16,17 @@ class KeyringUnavailableError(CredentialError): """Raised when keyring is requested but no backend is available.""" +class KeyringOperationError(CredentialError): + """Raised when a keyring read or write fails inside the backend.""" + + @dataclass(frozen=True) class TokenRequest: """Parameters describing which token we need.""" provider_kind: str # e.g. "gitea", "github" host: str # e.g. "git.example.org" or "github.com" - owner: Optional[str] = None # optional org/user + owner: str | None = None # optional org/user @dataclass(frozen=True)