diff --git a/keyring/backend.py b/keyring/backend.py index 1b903587..a1c2a9f3 100644 --- a/keyring/backend.py +++ b/keyring/backend.py @@ -1,6 +1,7 @@ """ Keyring implementation support """ +from __future__ import annotations import os import abc @@ -8,7 +9,7 @@ import operator import copy -from typing import Optional +import typing from .py312compat import metadata from . import credentials, errors, util @@ -18,7 +19,7 @@ by_priority = operator.attrgetter('priority') -_limit = None +_limit: typing.Optional[typing.Callable[[KeyringBackend], bool]] = None class KeyringBackendMeta(abc.ABCMeta): @@ -44,8 +45,8 @@ class KeyringBackend(metaclass=KeyringBackendMeta): def __init__(self): self.set_properties_from_env() - # @abc.abstractproperty - def priority(cls): + @properties.classproperty + def priority(self) -> typing.Union[int, float]: """ Each backend class must supply a priority, a number (float or integer) indicating the priority of the backend relative to all other backends. @@ -60,6 +61,7 @@ def priority(cls): As a rule of thumb, a priority between zero but less than one is suitable, but a priority of one or greater is recommended. """ + raise NotImplementedError @properties.classproperty def viable(cls): @@ -68,14 +70,16 @@ def viable(cls): return not exc @classmethod - def get_viable_backends(cls): + def get_viable_backends( + cls: typing.Type[KeyringBackend], + ) -> filter[typing.Type[KeyringBackend]]: """ Return all subclasses deemed viable. """ return filter(operator.attrgetter('viable'), cls._classes) @properties.classproperty - def name(cls): + def name(cls) -> str: """ The keyring name, suitable for display. @@ -83,16 +87,16 @@ def name(cls): """ parent, sep, mod_name = cls.__module__.rpartition('.') mod_name = mod_name.replace('_', ' ') - return ' '.join([mod_name, cls.__name__]) + return ' '.join([mod_name, cls.__name__]) # type: ignore - def __str__(self): + def __str__(self) -> str: keyring_class = type(self) return "{}.{} (priority: {:g})".format( keyring_class.__module__, keyring_class.__name__, keyring_class.priority ) @abc.abstractmethod - def get_password(self, service: str, username: str) -> Optional[str]: + def get_password(self, service: str, username: str) -> typing.Optional[str]: """Get password of the username for the service""" return None @@ -122,8 +126,8 @@ def delete_password(self, service: str, username: str) -> None: def get_credential( self, service: str, - username: Optional[str], - ) -> Optional[credentials.Credential]: + username: typing.Optional[str], + ) -> typing.Optional[credentials.Credential]: """Gets the username and password for the service. Returns a Credential instance. @@ -138,19 +142,21 @@ def get_credential( return credentials.SimpleCredential(username, password) return None - def set_properties_from_env(self): + def set_properties_from_env(self) -> None: """For all KEYRING_PROPERTY_* env var, set that property.""" - def parse(item): + def parse(item: typing.Tuple[str, str]): key, value = item pre, sep, name = key.partition('KEYRING_PROPERTY_') return sep and (name.lower(), value) - props = filter(None, map(parse, os.environ.items())) + props: filter[typing.Tuple[str, str]] = filter( + None, map(parse, os.environ.items()) + ) for name, value in props: setattr(self, name, value) - def with_properties(self, **kwargs): + def with_properties(self, **kwargs: typing.Any) -> KeyringBackend: alt = copy.copy(self) vars(alt).update(kwargs) return alt @@ -180,7 +186,7 @@ def decrypt(self, value): return value -def _load_plugins(): +def _load_plugins() -> None: """ Locate all setuptools entry points by the name 'keyring backends' and initialize them. @@ -207,7 +213,7 @@ def _load_plugins(): @util.once -def get_all_keyring(): +def get_all_keyring() -> typing.List[KeyringBackend]: """ Return a list of all implemented keyrings that can be constructed without parameters. @@ -241,7 +247,9 @@ class SchemeSelectable: KeePassXC=dict(username='UserName', service='Title'), ) - def _query(self, service, username=None, **base): + def _query( + self, service: str, username: typing.Optional[str] = None, **base: typing.Any + ) -> typing.Dict[str, str]: scheme = self.schemes[self.scheme] return dict( { diff --git a/keyring/backends/SecretService.py b/keyring/backends/SecretService.py index 7edb238d..bcd67165 100644 --- a/keyring/backends/SecretService.py +++ b/keyring/backends/SecretService.py @@ -30,7 +30,7 @@ class Keyring(backend.SchemeSelectable, KeyringBackend): appid = 'Python keyring library' @properties.classproperty - def priority(cls): + def priority(cls) -> int: with ExceptionRaisedContext() as exc: secretstorage.__name__ if exc: diff --git a/keyring/backends/Windows.py b/keyring/backends/Windows.py index a686d895..962c9a9a 100644 --- a/keyring/backends/Windows.py +++ b/keyring/backends/Windows.py @@ -81,7 +81,7 @@ class WinVaultKeyring(KeyringBackend): persist = Persistence() @properties.classproperty - def priority(cls): + def priority(cls) -> int: """ If available, the preferred backend on Windows. """ diff --git a/keyring/backends/chainer.py b/keyring/backends/chainer.py index 5a9281c7..42da1f0f 100644 --- a/keyring/backends/chainer.py +++ b/keyring/backends/chainer.py @@ -2,7 +2,6 @@ Keyring Chainer - iterates over other viable backends to discover passwords in each. """ - from .. import backend from .._compat import properties from . import fail @@ -19,7 +18,7 @@ class ChainerBackend(backend.KeyringBackend): viable = True @properties.classproperty - def priority(cls): + def priority(cls) -> int: """ If there are backends to chain, high priority Otherwise very low priority since our operation when empty diff --git a/keyring/backends/fail.py b/keyring/backends/fail.py index 179717a9..9bcdfc80 100644 --- a/keyring/backends/fail.py +++ b/keyring/backends/fail.py @@ -1,4 +1,5 @@ from ..backend import KeyringBackend +from .._compat import properties from ..errors import NoKeyringError @@ -13,7 +14,9 @@ class Keyring(KeyringBackend): keyring.errors.NoKeyringError: ...No recommended backend... """ - priority = 0 + @properties.classproperty + def priority(cls) -> int: + return 0 def get_password(self, service, username, password=None): msg = ( diff --git a/keyring/backends/kwallet.py b/keyring/backends/kwallet.py index 3137daed..9decb36c 100644 --- a/keyring/backends/kwallet.py +++ b/keyring/backends/kwallet.py @@ -38,7 +38,7 @@ class DBusKeyring(KeyringBackend): object_path = '/modules/kwalletd5' @properties.classproperty - def priority(cls): + def priority(cls) -> float: if 'dbus' not in globals(): raise RuntimeError('python-dbus not installed') try: diff --git a/keyring/backends/libsecret.py b/keyring/backends/libsecret.py index cf16dbec..5bc4f076 100644 --- a/keyring/backends/libsecret.py +++ b/keyring/backends/libsecret.py @@ -48,7 +48,7 @@ def collection(self): return Secret.COLLECTION_DEFAULT @properties.classproperty - def priority(cls): + def priority(cls) -> float: if not available: raise RuntimeError("libsecret required") diff --git a/keyring/backends/null.py b/keyring/backends/null.py index 6525c0ff..c2067b73 100644 --- a/keyring/backends/null.py +++ b/keyring/backends/null.py @@ -1,4 +1,5 @@ from ..backend import KeyringBackend +from .._compat import properties class Keyring(KeyringBackend): @@ -9,7 +10,9 @@ class Keyring(KeyringBackend): >>> kr.get_password('svc', 'user') """ - priority = -1 + @properties.classproperty + def priority(cls) -> int: + return -1 def get_password(self, service, username, password=None): pass diff --git a/keyring/core.py b/keyring/core.py index 38ca1f08..6ed119f7 100644 --- a/keyring/core.py +++ b/keyring/core.py @@ -12,13 +12,14 @@ from .util import platform_ as platform from .backends import fail +LimitCallable = typing.Callable[[backend.KeyringBackend], bool] log = logging.getLogger(__name__) _keyring_backend = None -def set_keyring(keyring): +def set_keyring(keyring: backend.KeyringBackend) -> None: """Set current keyring backend.""" global _keyring_backend if not isinstance(keyring, backend.KeyringBackend): @@ -33,7 +34,7 @@ def get_keyring() -> backend.KeyringBackend: return typing.cast(backend.KeyringBackend, _keyring_backend) -def disable(): +def disable() -> None: """ Configure the null keyring as the default. """ @@ -72,18 +73,18 @@ def get_credential( return get_keyring().get_credential(service_name, username) -def recommended(backend): +def recommended(backend) -> bool: return backend.priority >= 1 -def init_backend(limit=None): +def init_backend(limit: typing.Optional[LimitCallable] = None): """ Load a detected backend. """ set_keyring(_detect_backend(limit)) -def _detect_backend(limit=None): +def _detect_backend(limit: typing.Optional[LimitCallable] = None): """ Return a keyring specified in the config file or infer the best available. @@ -105,7 +106,7 @@ def _detect_backend(limit=None): ) -def _load_keyring_class(keyring_name): +def _load_keyring_class(keyring_name: str) -> typing.Type[backend.KeyringBackend]: """ Load the keyring class indicated by name. @@ -126,7 +127,7 @@ def _load_keyring_class(keyring_name): return getattr(module, class_name) -def load_keyring(keyring_name): +def load_keyring(keyring_name: str) -> backend.KeyringBackend: """ Load the specified keyring by name (a fully-qualified name to the keyring, such as 'keyring.backends.file.PlaintextKeyring') @@ -137,15 +138,15 @@ def load_keyring(keyring_name): return class_() -def load_env(): +def load_env() -> typing.Optional[backend.KeyringBackend]: """Load a keyring configured in the environment variable.""" try: return load_keyring(os.environ['PYTHON_KEYRING_BACKEND']) except KeyError: - pass + return None -def load_config(): +def load_config() -> typing.Optional[backend.KeyringBackend]: """Load a keyring using the config file in the config root.""" filename = 'keyringrc.cfg' @@ -153,7 +154,7 @@ def load_config(): keyring_cfg = os.path.join(platform.config_root(), filename) if not os.path.exists(keyring_cfg): - return + return None config = configparser.RawConfigParser() config.read(keyring_cfg) @@ -172,12 +173,12 @@ def load_config(): "Keyring config file contains incorrect values.\n" + "Config file: %s" % keyring_cfg ) - return + return None return load_keyring(keyring_name) -def _load_keyring_path(config): +def _load_keyring_path(config: configparser.RawConfigParser) -> None: "load the keyring-path option (if present)" try: path = config.get("backend", "keyring-path").strip()