Skip to content

Commit

Permalink
fix: handle keyring store exception
Browse files Browse the repository at this point in the history
  • Loading branch information
chidiwilliams committed Mar 14, 2024
1 parent 9395492 commit 0e15168
Show file tree
Hide file tree
Showing 10 changed files with 71 additions and 61 deletions.
6 changes: 2 additions & 4 deletions buzz/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
TranscriptionModel,
ModelDownloader,
)
from buzz.store.keyring_store import KeyringStore
from buzz.store.keyring_store import get_password, Key
from buzz.transcriber.transcriber import (
Task,
FileTranscriptionTask,
Expand Down Expand Up @@ -180,9 +180,7 @@ def parse(app: Application, parser: QCommandLineParser):
model.model_type == ModelType.OPEN_AI_WHISPER_API
and openai_access_token == ""
):
openai_access_token = KeyringStore().get_password(
key=KeyringStore.Key.OPENAI_API_KEY
)
openai_access_token = get_password(key=Key.OPENAI_API_KEY)

Check warning on line 183 in buzz/cli.py

View check run for this annotation

Codecov / codecov/patch

buzz/cli.py#L183

Added line #L183 was not covered by tests

if openai_access_token == "":
raise CommandLineError("No OpenAI access token found")
Expand Down
31 changes: 14 additions & 17 deletions buzz/store/keyring_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,24 @@
import logging

import keyring
from keyring.errors import KeyringLocked, KeyringError, PasswordSetError

from buzz.settings.settings import APP_NAME


class KeyringStore:
class Key(enum.Enum):
OPENAI_API_KEY = "OpenAI API key"
class Key(enum.Enum):
OPENAI_API_KEY = "OpenAI API key"

def get_password(self, key: Key) -> str:
try:
password = keyring.get_password(APP_NAME, username=key.value)
if password is None:
return ""
return password
except (KeyringLocked, KeyringError) as exc:
logging.warning("Unable to read from keyring: %s", exc)

def get_password(key: Key) -> str | None:
try:
password = keyring.get_password(APP_NAME, username=key.value)
if password is None:
return ""
return password

Check warning on line 18 in buzz/store/keyring_store.py

View check run for this annotation

Codecov / codecov/patch

buzz/store/keyring_store.py#L18

Added line #L18 was not covered by tests
except Exception as exc:
logging.warning("Unable to read from keyring: %s", exc)
return ""


def set_password(self, username: Key, password: str) -> None:
try:
keyring.set_password(APP_NAME, username.value, password)
except (KeyringLocked, PasswordSetError) as exc:
logging.error("Unable to write to keyring: %s", exc)
def set_password(username: Key, password: str) -> None:
keyring.set_password(APP_NAME, username.value, password)

Check warning on line 25 in buzz/store/keyring_store.py

View check run for this annotation

Codecov / codecov/patch

buzz/store/keyring_store.py#L25

Added line #L25 was not covered by tests
10 changes: 8 additions & 2 deletions buzz/widgets/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from buzz.locale import _
from buzz.settings.settings import APP_NAME, Settings
from buzz.settings.shortcut_settings import ShortcutSettings
from buzz.store.keyring_store import KeyringStore
from buzz.store.keyring_store import set_password, Key
from buzz.transcriber.transcriber import (
FileTranscriptionTask,
TranscriptionOptions,
Expand Down Expand Up @@ -248,7 +248,13 @@ def open_file_transcriber_widget(

@staticmethod
def on_openai_access_token_changed(access_token: str):
KeyringStore().set_password(KeyringStore.Key.OPENAI_API_KEY, access_token)
try:
set_password(Key.OPENAI_API_KEY, access_token)
except Exception as exc:
logging.error("Unable to write to keyring: %s", exc)
QMessageBox.critical(

Check warning on line 255 in buzz/widgets/main_window.py

View check run for this annotation

Codecov / codecov/patch

buzz/widgets/main_window.py#L251-L255

Added lines #L251 - L255 were not covered by tests
None, _("Error"), _("Unable to save OpenAI API key to keyring")
)

def open_transcript_viewer(self):
selected_rows = self.table_widget.selectionModel().selectedRows()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
QVBoxLayout,
)

from buzz.store.keyring_store import KeyringStore
from buzz.store.keyring_store import Key, get_password
from buzz.transcriber.transcriber import (
TranscriptionOptions,
FileTranscriptionOptions,
Expand Down Expand Up @@ -67,9 +67,7 @@ def __init__(
output_folder_row.addWidget(self.output_folder_line_edit)
output_folder_row.addWidget(output_folder_browse_button)

openai_access_token = KeyringStore().get_password(
KeyringStore.Key.OPENAI_API_KEY
)
openai_access_token = get_password(Key.OPENAI_API_KEY)
(
transcription_options,
file_transcription_options,
Expand Down
7 changes: 2 additions & 5 deletions buzz/widgets/preferences_dialog/general_preferences_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from openai import AuthenticationError, OpenAI

from buzz.settings.settings import Settings
from buzz.store.keyring_store import KeyringStore
from buzz.store.keyring_store import get_password, Key
from buzz.widgets.line_edit import LineEdit
from buzz.widgets.openai_api_key_line_edit import OpenAIAPIKeyLineEdit

Expand All @@ -15,14 +15,11 @@ class GeneralPreferencesWidget(QWidget):

def __init__(
self,
keyring_store=KeyringStore(),
parent: Optional[QWidget] = None,
):
super().__init__(parent)

self.openai_api_key = keyring_store.get_password(
KeyringStore.Key.OPENAI_API_KEY
)
self.openai_api_key = get_password(Key.OPENAI_API_KEY)

layout = QFormLayout(self)

Expand Down
6 changes: 2 additions & 4 deletions buzz/widgets/transcriber/file_transcriber_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from buzz.model_loader import ModelDownloader
from buzz.paths import file_path_as_title
from buzz.settings.settings import Settings
from buzz.store.keyring_store import KeyringStore
from buzz.store.keyring_store import get_password, Key
from buzz.transcriber.transcriber import (
FileTranscriptionOptions,
TranscriptionOptions,
Expand Down Expand Up @@ -52,9 +52,7 @@ def __init__(

self.setWindowTitle(self.get_title())

openai_access_token = KeyringStore().get_password(
KeyringStore.Key.OPENAI_API_KEY
)
openai_access_token = get_password(Key.OPENAI_API_KEY)

preferences = self.load_preferences()

Expand Down
6 changes: 2 additions & 4 deletions buzz/widgets/transcription_task_folder_watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from PyQt6.QtCore import QFileSystemWatcher, pyqtSignal, QObject

from buzz.store.keyring_store import KeyringStore
from buzz.store.keyring_store import Key, get_password
from buzz.transcriber.transcriber import FileTranscriptionTask
from buzz.widgets.preferences_dialog.models.folder_watch_preferences import (
FolderWatchPreferences,
Expand Down Expand Up @@ -49,9 +49,7 @@ def find_tasks(self):
):
continue

openai_access_token = KeyringStore().get_password(
KeyringStore.Key.OPENAI_API_KEY
)
openai_access_token = get_password(Key.OPENAI_API_KEY)
(
transcription_options,
file_transcription_options,
Expand Down
19 changes: 18 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pytest = "^7.1.3"
pytest-cov = "^4.0.0"
pytest-qt = "^4.1.0"
pytest-xvfb = "^2.0.0"
pytest-mock = "^3.12.0"
pylint = "^2.15.5"
pre-commit = "^2.20.0"
pytest-benchmark = "^4.0.0"
Expand Down
40 changes: 20 additions & 20 deletions tests/widgets/preferences_dialog/general_preferences_widget_test.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
from unittest.mock import Mock

from PyQt6.QtWidgets import QPushButton, QMessageBox, QLineEdit

from buzz.store.keyring_store import KeyringStore
from buzz.widgets.preferences_dialog.general_preferences_widget import (
GeneralPreferencesWidget,
)


class TestGeneralPreferencesWidget:
def test_should_disable_test_button_if_no_api_key(self, qtbot):
widget = GeneralPreferencesWidget(keyring_store=self.get_keyring_store(""))
def test_should_disable_test_button_if_no_api_key(self, qtbot, mocker):
mocker.patch(
"buzz.widgets.preferences_dialog.general_preferences_widget.get_password",
return_value="",
)

widget = GeneralPreferencesWidget()
qtbot.add_widget(widget)

test_button = widget.findChild(QPushButton)
Expand All @@ -25,32 +27,30 @@ def test_should_disable_test_button_if_no_api_key(self, qtbot):

assert test_button.isEnabled()

def test_should_test_openai_api_key(self, qtbot):
widget = GeneralPreferencesWidget(
keyring_store=self.get_keyring_store("wrong-api-key"),
def test_should_test_openai_api_key(self, qtbot, mocker):
mocker.patch(
"buzz.widgets.preferences_dialog.general_preferences_widget.get_password",
return_value="wrong-api-key",
)

widget = GeneralPreferencesWidget()
qtbot.add_widget(widget)

test_button = widget.findChild(QPushButton)
assert isinstance(test_button, QPushButton)

test_button.click()

mock = Mock()
QMessageBox.warning = mock
message_box_warning_mock = mocker.Mock()
QMessageBox.warning = message_box_warning_mock

def mock_called():
mock.assert_called()
assert mock.call_args[0][1] == "OpenAI API Key Test"
message_box_warning_mock.assert_called()
assert message_box_warning_mock.call_args[0][1] == "OpenAI API Key Test"
assert (
mock.call_args[0][2]
== "Incorrect API key provided: wrong-ap*-key. You can find your API key at https://platform.openai.com/account/api-keys."
message_box_warning_mock.call_args[0][2]
== "Incorrect API key provided: wrong-ap*-key. You can find your "
"API key at https://platform.openai.com/account/api-keys."
)

qtbot.waitUntil(mock_called)

@staticmethod
def get_keyring_store(password: str):
keyring_store = Mock(KeyringStore)
keyring_store.get_password.return_value = password
return keyring_store

0 comments on commit 0e15168

Please sign in to comment.