Skip to content

Commit

Permalink
Merge pull request #625 from lovetox/master
Browse files Browse the repository at this point in the history
Add more type annotations
  • Loading branch information
jaraco committed May 24, 2023
2 parents 9f7bcfc + 4582e76 commit a94ff95
Show file tree
Hide file tree
Showing 9 changed files with 53 additions and 39 deletions.
44 changes: 26 additions & 18 deletions keyring/backend.py
@@ -1,14 +1,15 @@
"""
Keyring implementation support
"""
from __future__ import annotations

import os
import abc
import logging
import operator
import copy

from typing import Optional
import typing

from .py312compat import metadata
from . import credentials, errors, util
Expand All @@ -18,7 +19,7 @@


by_priority = operator.attrgetter('priority')
_limit = None
_limit: typing.Optional[typing.Callable[[KeyringBackend], bool]] = None


class KeyringBackendMeta(abc.ABCMeta):
Expand All @@ -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.
Expand All @@ -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):
Expand All @@ -68,31 +70,33 @@ 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.
The name is derived from module and class name.
"""
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

Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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(
{
Expand Down
2 changes: 1 addition & 1 deletion keyring/backends/SecretService.py
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion keyring/backends/Windows.py
Expand Up @@ -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.
"""
Expand Down
3 changes: 1 addition & 2 deletions keyring/backends/chainer.py
Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion keyring/backends/fail.py
@@ -1,4 +1,5 @@
from ..backend import KeyringBackend
from .._compat import properties
from ..errors import NoKeyringError


Expand All @@ -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 = (
Expand Down
2 changes: 1 addition & 1 deletion keyring/backends/kwallet.py
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion keyring/backends/libsecret.py
Expand Up @@ -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")

Expand Down
5 changes: 4 additions & 1 deletion keyring/backends/null.py
@@ -1,4 +1,5 @@
from ..backend import KeyringBackend
from .._compat import properties


class Keyring(KeyringBackend):
Expand All @@ -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
Expand Down
27 changes: 14 additions & 13 deletions keyring/core.py
Expand Up @@ -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):
Expand All @@ -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.
"""
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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')
Expand All @@ -137,23 +138,23 @@ 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'

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)
Expand All @@ -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()
Expand Down

0 comments on commit a94ff95

Please sign in to comment.