Skip to content

Commit

Permalink
Added functions to check keyring service availability
Browse files Browse the repository at this point in the history
Details:

* In the 'KeyRingLib' class, added methods 'is_available()' and
  'check_available() that return whether the keyring service is available
  or check that it is available. (issue #34)

Signed-off-by: Andreas Maier <andreas.r.maier@gmx.de>
  • Loading branch information
andy-maier committed Apr 1, 2021
1 parent f2f0b80 commit 8551601
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 109 deletions.
12 changes: 12 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ Exception classes
:members:
:special-members: __str__

.. autoclass:: easy_vault.KeyRingException
:members:
:special-members: __str__

.. autoclass:: easy_vault.KeyRingNotAvailable
:members:
:special-members: __str__

.. autoclass:: easy_vault.KeyRingError
:members:
:special-members: __str__


.. _`Package version`:

Expand Down
4 changes: 4 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ Released: not yet
commands that silöences the messages except for the password prompt.
(issue #12)

* In the 'KeyRingLib' class, added methods 'is_available()' and
'check_available() that return whether the keyring service is available
or check that it is available. (issue #34)

* Test: Improved test coverage. (issue #8)

**Cleanup:**
Expand Down
130 changes: 119 additions & 11 deletions easy_vault/_key_ring_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,50 @@
import os
import keyring

__all__ = ['KeyRingLib']
__all__ = ['KeyRingLib', 'KeyRingException', 'KeyRingNotAvailable',
'KeyRingError']


# Service/app name used for keyring items:
# Version 1.0: The item value is the password
KEYRING_SERVICE = u'easy_vault/pypi.org/1.0'

# Exception to catch when no keyring service is available.
# Keyring version 22.0 introduced NoKeyringError and before that used
# RuntimeError.
try:
NO_KEYRING_EXCEPTION = keyring.errors.NoKeyringError
except AttributeError:
NO_KEYRING_EXCEPTION = RuntimeError


class KeyRingException(Exception):
"""
Base exception for all exceptions raised by the
:class:`~easy_vault.KeyRingLib` class.
Derived from :exc:`py:Exception`.
"""
pass


class KeyRingNotAvailable(KeyRingException):
"""
Exception indicating that the keyring service is not available.
Derived from :exc:`KeyRingException`.
"""
pass


class KeyRingError(KeyRingException):
"""
Exception indicating that an error happend in the keyring service.
Derived from :exc:`KeyRingException`.
"""
pass


class KeyRingLib(object):
"""
Expand Down Expand Up @@ -64,13 +101,16 @@ def get_password(self, filepath):
:term:`unicode string`: Password for the vault file, or `None`.
Raises:
:exc:`keyring:keyring.errors.NoKeyringError` or RuntimeError:
No keyring service available
:exc:`keyring:keyring.errors.KeyringError`: Base class for errors with
the keyring service
:exc:`KeyringNotAvailable`: No keyring service available.
:exc:`KeyringError`: An error happend in the keyring service.
"""
return keyring.get_password(
self.keyring_service(), self.keyring_username(filepath))
try:
return keyring.get_password(
self.keyring_service(), self.keyring_username(filepath))
except NO_KEYRING_EXCEPTION as exc:
raise KeyRingNotAvailable(str(exc))
except keyring.errors.KeyringError as exc:
raise KeyRingError(str(exc))

def set_password(self, filepath, password):
"""
Expand All @@ -86,14 +126,82 @@ def set_password(self, filepath, password):
Password for the vault file.
Raises:
:exc:`keyring:keyring.errors.NoKeyringError` or RuntimeError:
No keyring service available
:exc:`keyring:keyring.errors.KeyringError`: Base class for errors with
the keyring service
:exc:`KeyringNotAvailable`: No keyring service available.
:exc:`KeyringError`: An error happend in the keyring service.
"""
keyring.set_password(
self.keyring_service(), self.keyring_username(filepath), password)

def is_available(self):
"""
Indicate whether the keyring service is available on the local system.
This function reports this only as a boolean. If information about
the reasons for not being available is needed, use the
:meth:`check_available` method instead.
Returns:
bool: Keyring service is available on the local system.
"""
try:
self.check_available()
except KeyRingNotAvailable:
return False
return True

@staticmethod
def check_available():
"""
Check whether the keyring service is available on the local system.
If available, the method returns.
If not available, an exception is raised with a message that provides
some information about the keyring configuration.
Raises:
:exc:`KeyringNotAvailable`: No keyring service available.
"""

# Check the cases where the keyring package indicates it has no
# keyring service found or no backend configured.

backend = keyring.get_keyring()

if isinstance(backend, keyring.backends.chainer.ChainerBackend):
if not backend.backends:
raise KeyRingNotAvailable(
"No keyring service found by the configured backends")

if isinstance(backend, keyring.backends.fail.Keyring):
raise KeyRingNotAvailable(
"No keyring service found by the configured backends")

if isinstance(backend, keyring.backends.null.Keyring):
raise KeyRingNotAvailable(
"Keyring service disabled by a configured null backend")

# In theory, now the keyring service should be available.
# We try it out to really make sure.

keyringlib = KeyRingLib()
service = keyringlib.keyring_service()
username = keyringlib.keyring_username('deleteme:check_available')
try:
keyring.set_password(service, username, 'dummy')
except NO_KEYRING_EXCEPTION as exc:
new_exc = KeyRingNotAvailable(
"Keyring test call failed with: {msg}".format(msg=exc))
new_exc.__cause__ = None
raise new_exc # KeyRingNotAvailable
try:
keyring.delete_password(service, username)
except Exception as exc:
new_exc = KeyRingNotAvailable(
"Keyring cleanup call failed with: {msg}".format(msg=exc))
new_exc.__cause__ = None
raise new_exc # KeyRingNotAvailable

@staticmethod
def keyring_service():
"""
Expand Down
24 changes: 23 additions & 1 deletion tests/unittest/test_key_ring_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from __future__ import absolute_import, print_function

import pytest
from easy_vault import KeyRingLib
from easy_vault import KeyRingLib, KeyRingNotAvailable

# pylint: disable=unused-import
from ..utils.keyring_utils import keyring_filepath # noqa: F401
Expand All @@ -44,3 +44,25 @@ def test_keyringlib_get_set(keyring_filepath):
# Test that getting a password succeeds and is as expected
act_password = keyringlib.get_password(keyring_filepath)
assert act_password == password


def test_keyringlib_available():
"""
Test function for KeyRingLib.is_available()
"""
keyringlib = KeyRingLib()

# Code to be tested
is_avail = keyringlib.is_available()

assert isinstance(is_avail, bool)

check_avail = True
try:
# Code to be tested
keyringlib.check_available()
except Exception as exc: # pylint: disable=broad-except
assert isinstance(exc, KeyRingNotAvailable)
check_avail = False

assert check_avail == is_avail
Empty file removed tests/unittest/utils/__init__.py
Empty file.
63 changes: 0 additions & 63 deletions tests/unittest/utils/test_keyring_utils.py

This file was deleted.

35 changes: 1 addition & 34 deletions tests/utils/keyring_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,41 +42,8 @@ def is_keyring_available():
Return boolean indicating whether the keyring service is available on
the local system.
"""

# Check some obvious cases where no keyring service is available

backend = keyring.get_keyring()
if isinstance(backend, keyring.backends.chainer.ChainerBackend):
if not backend.backends:
# Chainer backend with empty list of real backends
return False
if isinstance(backend, keyring.backends.fail.Keyring):
return False
if isinstance(backend, keyring.backends.null.Keyring):
return False

# In theory, now the keyring service should be available.
# We try it out to make really sure.

keyringlib = KeyRingLib()
service = keyringlib.keyring_service()
username = keyringlib.keyring_username('test/is_keyring_available')

# Exception to catch when no keyring service is available.
# Keyring version 22.0 introduced NoKeyringError and before that used
# RuntimeError.
try:
no_keyring_exception = keyring.errors.NoKeyringError
except AttributeError:
no_keyring_exception = RuntimeError

try:
keyring.set_password(service, username, 'dummy')
except no_keyring_exception:
return False
else:
keyring.delete_password(service, username)
return True
return keyringlib.is_available()


def remove_keyring_item(filepath):
Expand Down

0 comments on commit 8551601

Please sign in to comment.