From 6188dfbfdc40658f2c6de880635596e3f9441faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Sat, 8 Aug 2026 11:08:12 +0200 Subject: [PATCH 1/6] libsecret: Add type annotations --- keyring/backends/libsecret.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/keyring/backends/libsecret.py b/keyring/backends/libsecret.py index b92b3c22..751aa5be 100644 --- a/keyring/backends/libsecret.py +++ b/keyring/backends/libsecret.py @@ -1,9 +1,11 @@ import logging +import typing from .. import backend from ..backend import KeyringBackend from ..compat import properties from ..credentials import SimpleCredential +from ..credentials import Credential from ..errors import ( KeyringLocked, PasswordDeleteError, @@ -20,7 +22,10 @@ available = True except (AttributeError, ImportError, ValueError): - pass + if typing.TYPE_CHECKING: + from gi.repository import GLib + from gi.repository import Gio + from gi.repository import Secret log = logging.getLogger(__name__) @@ -31,7 +36,7 @@ class Keyring(backend.SchemeSelectable, KeyringBackend): appid = 'Python keyring library' @property - def schema(self): + def schema(self) -> Secret.Schema: return Secret.Schema.new( "org.freedesktop.Secret.Generic", Secret.SchemaFlags.NONE, @@ -43,7 +48,7 @@ def schema(self): ) @properties.NonDataProperty - def collection(self): + def collection(self) -> str: return Secret.COLLECTION_DEFAULT @properties.classproperty @@ -59,7 +64,7 @@ def priority(cls) -> float: return 4.8 - def get_password(self, service, username): + def get_password(self, service: str, username: str) -> str | None: """Get password of the username for the service""" attributes = self._query(service, username, application=self.appid) try: @@ -80,7 +85,9 @@ def get_password(self, service, username): raise KeyringLocked('Failed to unlock the item!') from error raise - def set_password(self, service, username, password): + return None + + def set_password(self, service: str, username: str, password: str) -> None: """Set password for the username of the service""" attributes = self._query(service, username, application=self.appid) label = f"Password for '{username}' on '{service}'" @@ -99,7 +106,7 @@ def set_password(self, service, username, password): if not stored: raise PasswordSetError("Failed to store password!") - def delete_password(self, service, username): + def delete_password(self, service: str, username: str) -> None: """Delete the stored password (only the first one)""" attributes = self._query(service, username, application=self.appid) try: @@ -124,7 +131,7 @@ def delete_password(self, service, username): return removed raise PasswordDeleteError("No such password!") - def get_credential(self, service, username): + def get_credential(self, service: str, username: str | None) -> Credential | None: """Get the first username and password for a service. Return a Credential instance @@ -153,3 +160,5 @@ def get_credential(self, service, username): if error.matches(quark, Secret.Error.IS_LOCKED): raise KeyringLocked('Failed to unlock the item!') from error raise + + return None From 1778d77f090987319b8f18ed6e23c3c23ff8609b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Sat, 8 Aug 2026 11:12:39 +0200 Subject: [PATCH 2/6] libsecret: Make delete_password() return None KeyringBackend defines that implementations should return None --- keyring/backends/libsecret.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keyring/backends/libsecret.py b/keyring/backends/libsecret.py index 751aa5be..bc9b936a 100644 --- a/keyring/backends/libsecret.py +++ b/keyring/backends/libsecret.py @@ -120,7 +120,7 @@ def delete_password(self, service: str, username: str) -> None: raise for item in items: try: - removed = Secret.password_clear_sync( + Secret.password_clear_sync( self.schema, item.get_attributes(), None ) except GLib.Error as error: @@ -128,7 +128,7 @@ def delete_password(self, service: str, username: str) -> None: if error.matches(quark, Secret.Error.IS_LOCKED): raise KeyringLocked('Failed to unlock the item!') from error raise - return removed + raise PasswordDeleteError("No such password!") def get_credential(self, service: str, username: str | None) -> Credential | None: From 856aadd522a23847f89a5ec3ab482ec02290bd18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Sat, 8 Aug 2026 11:16:48 +0200 Subject: [PATCH 3/6] libsecret: Add helper method to retrieve text This makes sure we never return None, and helps type checkers. Its not expected that retrieve_secret_sync() and get_text() ever return None, so this case is not handled. --- keyring/backends/libsecret.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/keyring/backends/libsecret.py b/keyring/backends/libsecret.py index bc9b936a..821941c1 100644 --- a/keyring/backends/libsecret.py +++ b/keyring/backends/libsecret.py @@ -64,6 +64,13 @@ def priority(cls) -> float: return 4.8 + def _get_text_from_item(self, item: Secret.Retrievable) -> str: + value = item.retrieve_secret_sync() + assert value is not None + text = value.get_text() + assert text is not None + return text + def get_password(self, service: str, username: str) -> str | None: """Get password of the username for the service""" attributes = self._query(service, username, application=self.appid) @@ -78,7 +85,7 @@ def get_password(self, service: str, username: str) -> str | None: raise for item in items: try: - return item.retrieve_secret_sync().get_text() + return self._get_text_from_item(item) except GLib.Error as error: quark = GLib.quark_try_string('secret-error') if error.matches(quark, Secret.Error.IS_LOCKED): @@ -150,11 +157,9 @@ def get_credential(self, service: str, username: str | None) -> Credential | Non raise KeyringLocked('Failed to unlock the item!') from error raise for item in items: - username = item.get_attributes().get("username") + username = item.get_attributes()["username"] try: - return SimpleCredential( - username, item.retrieve_secret_sync().get_text() - ) + return SimpleCredential(username, self._get_text_from_item(item)) except GLib.Error as error: quark = GLib.quark_try_string('secret-error') if error.matches(quark, Secret.Error.IS_LOCKED): From d85d5bb3f8ac2f7c2880fdeebf05202bea65fabc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Sun, 16 Aug 2026 09:13:26 +0200 Subject: [PATCH 4/6] backend: Make schema query arguments generic This allows the libsecret module to use this method also to create the Secret.Schema without typing errors --- keyring/backend.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/keyring/backend.py b/keyring/backend.py index 4f91e16e..46f7e55d 100644 --- a/keyring/backend.py +++ b/keyring/backend.py @@ -25,6 +25,7 @@ by_priority = operator.attrgetter('priority') _limit: typing.Callable[[KeyringBackend], bool] | None = None +_QT = typing.TypeVar("_QT") class KeyringBackendMeta(abc.ABCMeta): @@ -284,8 +285,8 @@ class SchemeSelectable: ) def _query( - self, service: str, username: str | None = None, **base: typing.Any - ) -> dict[str, str]: + self, service: _QT, username: _QT | None = None, **base: typing.Any + ) -> dict[str, _QT]: scheme = self.schemes[self.scheme] return dict( { From ed33e520b9b2ff5762ff0635164f11f3165393db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Sun, 16 Aug 2026 10:01:23 +0200 Subject: [PATCH 5/6] libsecret: Honor selected scheme in get_credential() --- keyring/backends/libsecret.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/keyring/backends/libsecret.py b/keyring/backends/libsecret.py index 821941c1..26b46c9d 100644 --- a/keyring/backends/libsecret.py +++ b/keyring/backends/libsecret.py @@ -147,6 +147,8 @@ def get_credential(self, service: str, username: str | None) -> Credential | Non Otherwise, it will return the first username and password combo that it finds. """ query = self._query(service, username) + scheme = self.schemes[self.scheme] + try: items = Secret.password_search_sync( self.schema, query, Secret.SearchFlags.UNLOCK, None @@ -157,7 +159,7 @@ def get_credential(self, service: str, username: str | None) -> Credential | Non raise KeyringLocked('Failed to unlock the item!') from error raise for item in items: - username = item.get_attributes()["username"] + username = item.get_attributes()[scheme["username"]] try: return SimpleCredential(username, self._get_text_from_item(item)) except GLib.Error as error: From 32ec9bf40cd3f434944f027d3abf9b89951cccca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Sun, 16 Aug 2026 13:41:32 +0200 Subject: [PATCH 6/6] libsecret: Ruff: Fix errors and format --- keyring/backends/libsecret.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/keyring/backends/libsecret.py b/keyring/backends/libsecret.py index 26b46c9d..54742772 100644 --- a/keyring/backends/libsecret.py +++ b/keyring/backends/libsecret.py @@ -4,8 +4,7 @@ from .. import backend from ..backend import KeyringBackend from ..compat import properties -from ..credentials import SimpleCredential -from ..credentials import Credential +from ..credentials import Credential, SimpleCredential from ..errors import ( KeyringLocked, PasswordDeleteError, @@ -23,9 +22,11 @@ available = True except (AttributeError, ImportError, ValueError): if typing.TYPE_CHECKING: - from gi.repository import GLib - from gi.repository import Gio - from gi.repository import Secret + from gi.repository import ( + Gio, # noqa: TC004 + GLib, # noqa: TC004 + Secret, # noqa: TC004 + ) log = logging.getLogger(__name__) @@ -127,9 +128,7 @@ def delete_password(self, service: str, username: str) -> None: raise for item in items: try: - Secret.password_clear_sync( - self.schema, item.get_attributes(), None - ) + Secret.password_clear_sync(self.schema, item.get_attributes(), None) except GLib.Error as error: quark = GLib.quark_try_string('secret-error') if error.matches(quark, Secret.Error.IS_LOCKED):