From 8142e4a48a4447e07ac252025eb3cc84158b1ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20M=C3=A9taireau?= Date: Mon, 24 Mar 2025 15:42:59 +0100 Subject: [PATCH] make dangerzone.settings a global --- dangerzone/gui/logic.py | 4 +- dangerzone/gui/main_window.py | 78 +++++++++------------ dangerzone/gui/updater.py | 20 +++--- dangerzone/logic.py | 5 +- dangerzone/settings.py | 127 +++++++++++++++++----------------- tests/conftest.py | 21 ++++++ tests/gui/conftest.py | 15 ++-- tests/gui/test_main_window.py | 54 +++++++-------- tests/gui/test_updater.py | 75 ++++++++++---------- tests/test_settings.py | 57 +++++---------- 10 files changed, 215 insertions(+), 241 deletions(-) diff --git a/dangerzone/gui/logic.py b/dangerzone/gui/logic.py index b8403b1..3ec4c77 100644 --- a/dangerzone/gui/logic.py +++ b/dangerzone/gui/logic.py @@ -12,6 +12,8 @@ from typing import Optional from colorama import Fore +from .. import settings + # FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details. if typing.TYPE_CHECKING: from PySide2 import QtCore, QtGui, QtWidgets @@ -80,7 +82,7 @@ class DangerzoneGui(DangerzoneCore): elif platform.system() == "Linux": # Get the PDF reader command - args = shlex.split(self.pdf_viewers[self.settings.get("open_app")]) + args = shlex.split(self.pdf_viewers[settings.get("open_app")]) # %f, %F, %u, and %U are filenames or URLS -- so replace with the file to open for i in range(len(args)): if ( diff --git a/dangerzone/gui/main_window.py b/dangerzone/gui/main_window.py index 0adc9c2..8c3ea78 100644 --- a/dangerzone/gui/main_window.py +++ b/dangerzone/gui/main_window.py @@ -8,6 +8,8 @@ from multiprocessing.pool import ThreadPool from pathlib import Path from typing import List, Optional +from .. import settings + # FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details. if typing.TYPE_CHECKING: from PySide2 import QtCore, QtGui, QtSvg, QtWidgets @@ -163,9 +165,7 @@ class MainWindow(QtWidgets.QMainWindow): self.toggle_updates_action = hamburger_menu.addAction("Check for updates") self.toggle_updates_action.triggered.connect(self.toggle_updates_triggered) self.toggle_updates_action.setCheckable(True) - self.toggle_updates_action.setChecked( - bool(self.dangerzone.settings.get("updater_check")) - ) + self.toggle_updates_action.setChecked(bool(settings.get("updater_check"))) # Add the "Exit" action hamburger_menu.addSeparator() @@ -231,8 +231,8 @@ class MainWindow(QtWidgets.QMainWindow): def show_update_success(self) -> None: """Inform the user about a new Dangerzone release.""" - version = self.dangerzone.settings.get("updater_latest_version") - changelog = self.dangerzone.settings.get("updater_latest_changelog") + version = settings.get("updater_latest_version") + changelog = settings.get("updater_latest_changelog") changelog_widget = CollapsibleBox("What's New?") changelog_layout = QtWidgets.QVBoxLayout() @@ -277,8 +277,8 @@ class MainWindow(QtWidgets.QMainWindow): def toggle_updates_triggered(self) -> None: """Change the underlying update check settings based on the user's choice.""" check = self.toggle_updates_action.isChecked() - self.dangerzone.settings.set("updater_check", check) - self.dangerzone.settings.save() + settings.set("updater_check", check) + settings.save() def handle_docker_desktop_version_check( self, is_version_valid: bool, version: str @@ -328,16 +328,16 @@ class MainWindow(QtWidgets.QMainWindow): """ # If there are no new updates, reset the error counter (if any) and return. if report.empty(): - self.dangerzone.settings.set("updater_errors", 0, autosave=True) + settings.set("updater_errors", 0, autosave=True) return hamburger_menu = self.hamburger_button.menu() if report.error: log.error(f"Encountered an error during an update check: {report.error}") - errors = self.dangerzone.settings.get("updater_errors") + 1 - self.dangerzone.settings.set("updater_errors", errors) - self.dangerzone.settings.save() + errors = settings.get("updater_errors") + 1 + settings.set("updater_errors", errors) + settings.save() self.updater_error = report.error # If we encounter more than three errors in a row, show a red notification @@ -369,13 +369,13 @@ class MainWindow(QtWidgets.QMainWindow): hamburger_menu.insertAction(sep, error_action) else: log.debug(f"Handling new version: {report.version}") - self.dangerzone.settings.set("updater_latest_version", report.version) - self.dangerzone.settings.set("updater_latest_changelog", report.changelog) - self.dangerzone.settings.set("updater_errors", 0) + settings.set("updater_latest_version", report.version) + settings.set("updater_latest_changelog", report.changelog) + settings.set("updater_errors", 0) # FIXME: Save the settings to the filesystem only when they have really changed, # maybe with a dirty bit. - self.dangerzone.settings.save() + settings.save() self.hamburger_button.setIcon( QtGui.QIcon( @@ -985,39 +985,37 @@ class SettingsWidget(QtWidgets.QWidget): self.setLayout(layout) # Load values from settings - if self.dangerzone.settings.get("save"): + if settings.get("save"): self.save_checkbox.setCheckState(QtCore.Qt.Checked) else: self.save_checkbox.setCheckState(QtCore.Qt.Unchecked) - if self.dangerzone.settings.get("safe_extension"): - self.safe_extension.setText(self.dangerzone.settings.get("safe_extension")) + if settings.get("safe_extension"): + self.safe_extension.setText(settings.get("safe_extension")) else: self.safe_extension.setText(SAFE_EXTENSION) - if self.dangerzone.settings.get("archive"): + if settings.get("archive"): self.radio_move_untrusted.setChecked(True) else: self.radio_save_to.setChecked(True) - if self.dangerzone.settings.get("ocr"): + if settings.get("ocr"): self.ocr_checkbox.setCheckState(QtCore.Qt.Checked) else: self.ocr_checkbox.setCheckState(QtCore.Qt.Unchecked) - index = self.ocr_combobox.findText(self.dangerzone.settings.get("ocr_language")) + index = self.ocr_combobox.findText(settings.get("ocr_language")) if index != -1: self.ocr_combobox.setCurrentIndex(index) - if self.dangerzone.settings.get("open"): + if settings.get("open"): self.open_checkbox.setCheckState(QtCore.Qt.Checked) else: self.open_checkbox.setCheckState(QtCore.Qt.Unchecked) if platform.system() == "Linux": - index = self.open_combobox.findText( - self.dangerzone.settings.get("open_app") - ) + index = self.open_combobox.findText(settings.get("open_app")) if index != -1: self.open_combobox.setCurrentIndex(index) @@ -1138,21 +1136,15 @@ class SettingsWidget(QtWidgets.QWidget): document.output_filename = tmp # Update settings - self.dangerzone.settings.set( - "save", self.save_checkbox.checkState() == QtCore.Qt.Checked - ) - self.dangerzone.settings.set("safe_extension", self.safe_extension.text()) - self.dangerzone.settings.set("archive", self.radio_move_untrusted.isChecked()) - self.dangerzone.settings.set( - "ocr", self.ocr_checkbox.checkState() == QtCore.Qt.Checked - ) - self.dangerzone.settings.set("ocr_language", self.ocr_combobox.currentText()) - self.dangerzone.settings.set( - "open", self.open_checkbox.checkState() == QtCore.Qt.Checked - ) + settings.set("save", self.save_checkbox.checkState() == QtCore.Qt.Checked) + settings.set("safe_extension", self.safe_extension.text()) + settings.set("archive", self.radio_move_untrusted.isChecked()) + settings.set("ocr", self.ocr_checkbox.checkState() == QtCore.Qt.Checked) + settings.set("ocr_language", self.ocr_combobox.currentText()) + settings.set("open", self.open_checkbox.checkState() == QtCore.Qt.Checked) if platform.system() == "Linux": - self.dangerzone.settings.set("open_app", self.open_combobox.currentText()) - self.dangerzone.settings.save() + settings.set("open_app", self.open_combobox.currentText()) + settings.save() # Start! self.start_clicked.emit() @@ -1234,10 +1226,8 @@ class DocumentsListWidget(QtWidgets.QListWidget): def get_ocr_lang(self) -> Optional[str]: ocr_lang = None - if self.dangerzone.settings.get("ocr"): - ocr_lang = self.dangerzone.ocr_languages[ - self.dangerzone.settings.get("ocr_language") - ] + if settings.get("ocr"): + ocr_lang = self.dangerzone.ocr_languages[settings.get("ocr_language")] return ocr_lang @@ -1326,7 +1316,7 @@ class DocumentWidget(QtWidgets.QWidget): return # Open - if self.dangerzone.settings.get("open"): + if settings.get("open"): self.dangerzone.open_pdf_viewer(self.document.output_filename) diff --git a/dangerzone/gui/updater.py b/dangerzone/gui/updater.py index 396de21..26a7f0d 100644 --- a/dangerzone/gui/updater.py +++ b/dangerzone/gui/updater.py @@ -10,6 +10,8 @@ from typing import Optional from packaging import version +from .. import settings + if typing.TYPE_CHECKING: from PySide2 import QtCore, QtWidgets else: @@ -126,11 +128,11 @@ class UpdaterThread(QtCore.QThread): @property def check(self) -> Optional[bool]: - return self.dangerzone.settings.get("updater_check") + return settings.get("updater_check") @check.setter def check(self, val: bool) -> None: - self.dangerzone.settings.set("updater_check", val, autosave=True) + settings.set("updater_check", val, autosave=True) def prompt_for_checks(self) -> Optional[bool]: """Ask the user if they want to be informed about Dangerzone updates.""" @@ -169,9 +171,9 @@ class UpdaterThread(QtCore.QThread): return False log.debug("Checking if first run of Dangerzone") - if self.dangerzone.settings.get("updater_last_check") is None: + if settings.get("updater_last_check") is None: log.debug("Dangerzone is running for the first time, updates are stalled") - self.dangerzone.settings.set("updater_last_check", 0, autosave=True) + settings.set("updater_last_check", 0, autosave=True) return False log.debug("Checking if user has already expressed their preference") @@ -204,7 +206,7 @@ class UpdaterThread(QtCore.QThread): again. """ current_time = self._get_now_timestamp() - last_check = self.dangerzone.settings.get("updater_last_check") + last_check = settings.get("updater_last_check") if current_time < last_check + UPDATE_CHECK_COOLDOWN_SECS: log.debug("Cooling down update checks") return True @@ -256,12 +258,12 @@ class UpdaterThread(QtCore.QThread): 2. In GitHub, by hitting the latest releases API. """ log.debug("Checking for Dangerzone updates") - latest_version = self.dangerzone.settings.get("updater_latest_version") + latest_version = settings.get("updater_latest_version") if version.parse(get_version()) < version.parse(latest_version): log.debug("Determined that there is an update due to cached results") return UpdateReport( version=latest_version, - changelog=self.dangerzone.settings.get("updater_latest_changelog"), + changelog=settings.get("updater_latest_changelog"), ) # If the previous check happened before the cooldown period expires, do not @@ -271,9 +273,7 @@ class UpdaterThread(QtCore.QThread): if self._should_postpone_update_check(): return UpdateReport() else: - self.dangerzone.settings.set( - "updater_last_check", self._get_now_timestamp(), autosave=True - ) + settings.set("updater_last_check", self._get_now_timestamp(), autosave=True) log.debug("Checking the latest GitHub release") report = self.get_latest_info() diff --git a/dangerzone/logic.py b/dangerzone/logic.py index 74884af..47e6ecf 100644 --- a/dangerzone/logic.py +++ b/dangerzone/logic.py @@ -5,10 +5,9 @@ from typing import Callable, List, Optional import colorama -from . import errors, util +from . import errors, settings, util from .document import Document from .isolation_provider.base import IsolationProvider -from .settings import Settings from .util import get_resource_path log = logging.getLogger(__name__) @@ -28,8 +27,6 @@ class DangerzoneCore(object): unsorted_ocr_languages = json.load(f) self.ocr_languages = dict(sorted(unsorted_ocr_languages.items())) - # Load settings - self.settings = Settings() self.documents: List[Document] = [] self.isolation_provider = isolation_provider diff --git a/dangerzone/settings.py b/dangerzone/settings.py index 4c8b939..52fb8aa 100644 --- a/dangerzone/settings.py +++ b/dangerzone/settings.py @@ -10,81 +10,78 @@ from .util import get_config_dir, get_version log = logging.getLogger(__name__) -SETTINGS_FILENAME: str = "settings.json" +FILENAME = get_config_dir() / "settings.json" + +SETTINGS: dict = {} -class Settings: - settings: Dict[str, Any] +def generate_default_settings() -> Dict[str, Any]: + return { + "save": True, + "archive": True, + "ocr": True, + "ocr_language": "English", + "open": True, + "open_app": None, + "safe_extension": SAFE_EXTENSION, + "updater_check": None, + "updater_last_check": None, # last check in UNIX epoch (secs since 1970) + # FIXME: How to invalidate those if they change upstream? + "updater_latest_version": get_version(), + "updater_latest_changelog": "", + "updater_errors": 0, + } - def __init__(self) -> None: - self.settings_filename = get_config_dir() / SETTINGS_FILENAME - self.default_settings: Dict[str, Any] = self.generate_default_settings() - self.load() - @classmethod - def generate_default_settings(cls) -> Dict[str, Any]: - return { - "save": True, - "archive": True, - "ocr": True, - "ocr_language": "English", - "open": True, - "open_app": None, - "safe_extension": SAFE_EXTENSION, - "updater_check": None, - "updater_last_check": None, # last check in UNIX epoch (secs since 1970) - # FIXME: How to invalidate those if they change upstream? - "updater_latest_version": get_version(), - "updater_latest_changelog": "", - "updater_errors": 0, - } +def get(key: str) -> Any: + return SETTINGS[key] - def get(self, key: str) -> Any: - return self.settings[key] - def set(self, key: str, val: Any, autosave: bool = False) -> None: +def set(key: str, val: Any, autosave: bool = False) -> None: + global SETTINGS + try: + old_val = SETTINGS.get(key) + except KeyError: + old_val = None + SETTINGS[key] = val + if autosave and val != old_val: + save() + + +def get_updater_settings() -> Dict[str, Any]: + return {key: val for key, val in SETTINGS.items() if key.startswith("updater_")} + + +def load() -> None: + global SETTINGS + default_settings = generate_default_settings() + if FILENAME.is_file() and FILENAME.exists(): + # If the settings file exists, load it try: - old_val = self.get(key) - except KeyError: - old_val = None - self.settings[key] = val - if autosave and val != old_val: - self.save() + with FILENAME.open("r") as settings_file: + SETTINGS = json.load(settings_file) - def get_updater_settings(self) -> Dict[str, Any]: - return { - key: val for key, val in self.settings.items() if key.startswith("updater_") - } + # If it's missing any fields, add them from the default settings + for key in default_settings: + if key not in SETTINGS: + SETTINGS[key] = default_settings[key] + elif key == "updater_latest_version": + if version.parse(get_version()) > version.parse(get(key)): + set(key, get_version()) - def load(self) -> None: - if os.path.isfile(self.settings_filename): - self.settings = self.default_settings + except Exception: + log.error("Error loading settings, falling back to default") + SETTINGS = default_settings - # If the settings file exists, load it - try: - with open(self.settings_filename, "r") as settings_file: - self.settings = json.load(settings_file) + else: + # Save with default settings + log.info("Settings file doesn't exist, starting with default") + SETTINGS = default_settings - # If it's missing any fields, add them from the default settings - for key in self.default_settings: - if key not in self.settings: - self.settings[key] = self.default_settings[key] - elif key == "updater_latest_version": - if version.parse(get_version()) > version.parse(self.get(key)): - self.set(key, get_version()) + save() - except Exception: - log.error("Error loading settings, falling back to default") - self.settings = self.default_settings - else: - # Save with default settings - log.info("Settings file doesn't exist, starting with default") - self.settings = self.default_settings - - self.save() - - def save(self) -> None: - self.settings_filename.parent.mkdir(parents=True, exist_ok=True) - with self.settings_filename.open("w") as settings_file: - json.dump(self.settings, settings_file, indent=4) +def save() -> None: + FILENAME.parent.mkdir(parents=True, exist_ok=True) + with FILENAME.open("w") as settings_file: + json.dump(SETTINGS, settings_file, indent=4) diff --git a/tests/conftest.py b/tests/conftest.py index 3bef1af..3b80cd7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,7 @@ from pathlib import Path from typing import Callable, List import pytest +from pytest_mock import MockerFixture from dangerzone.document import SAFE_EXTENSION from dangerzone.gui import Application @@ -111,6 +112,26 @@ def sample_pdf() -> str: return str(test_docs_dir.joinpath(BASIC_SAMPLE_PDF)) +@pytest.fixture +def mock_settings(mocker: MockerFixture, tmp_path: Path) -> Path: + mocker.patch("dangerzone.settings.FILENAME", tmp_path / "settings.json") + return tmp_path + + +@pytest.fixture +def default_settings_0_4_1() -> dict: + """Get the default settings for the 0.4.1 Dangerzone release.""" + return { + "save": True, + "archive": True, + "ocr": True, + "ocr_language": "English", + "open": True, + "open_app": None, + "safe_extension": "-safe.pdf", + } + + SAMPLE_DIRECTORY = "test_docs" BASIC_SAMPLE_PDF = "sample-pdf.pdf" BASIC_SAMPLE_DOC = "sample-doc.doc" diff --git a/tests/gui/conftest.py b/tests/gui/conftest.py index cbf49de..efcf69d 100644 --- a/tests/gui/conftest.py +++ b/tests/gui/conftest.py @@ -30,25 +30,20 @@ def generate_isolated_updater( else: app = get_qt_app() - dummy = Dummy() - # XXX: We can monkey-patch global state without wrapping it in a context manager, or - # worrying that it will leak between tests, for two reasons: - # - # 1. Parallel tests in PyTest take place in different processes. - # 2. The monkeypatch fixture tears down the monkey-patch after each test ends. - monkeypatch.setattr(util, "get_config_dir", lambda: tmp_path) - dangerzone = DangerzoneGui(app, isolation_provider=dummy) + dangerzone = DangerzoneGui(app, isolation_provider=Dummy()) updater = UpdaterThread(dangerzone) return updater @pytest.fixture def updater( - tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture + tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture, mock_settings: Path ) -> UpdaterThread: return generate_isolated_updater(tmp_path, monkeypatch, mocker) @pytest.fixture -def qt_updater(tmp_path: Path, monkeypatch: MonkeyPatch) -> UpdaterThread: +def qt_updater( + tmp_path: Path, monkeypatch: MonkeyPatch, mock_settings: Path +) -> UpdaterThread: return generate_isolated_updater(tmp_path, monkeypatch) diff --git a/tests/gui/test_main_window.py b/tests/gui/test_main_window.py index e4fc127..11b1f8f 100644 --- a/tests/gui/test_main_window.py +++ b/tests/gui/test_main_window.py @@ -3,13 +3,14 @@ import pathlib import platform import shutil import time +from pathlib import Path from typing import List from pytest import MonkeyPatch, fixture from pytest_mock import MockerFixture from pytestqt.qtbot import QtBot -from dangerzone import errors +from dangerzone import errors, settings from dangerzone.document import Document from dangerzone.gui import MainWindow from dangerzone.gui import main_window as main_window_module @@ -96,7 +97,7 @@ def test_default_menu( updater: UpdaterThread, ) -> None: """Check that the default menu entries are in order.""" - updater.dangerzone.settings.set("updater_check", True) + settings.set("updater_check", True) window = MainWindow(updater.dangerzone) menu_actions = window.hamburger_button.menu().actions() @@ -114,7 +115,7 @@ def test_default_menu( toggle_updates_action.trigger() assert not toggle_updates_action.isChecked() - assert updater.dangerzone.settings.get("updater_check") is False + assert settings.get("updater_check") is False def test_no_update( @@ -127,9 +128,9 @@ def test_no_update( # Check that when no update is detected, e.g., due to update cooldown, an empty # report is received that does not affect the menu entries. curtime = int(time.time()) - updater.dangerzone.settings.set("updater_check", True) - updater.dangerzone.settings.set("updater_errors", 9) - updater.dangerzone.settings.set("updater_last_check", curtime) + settings.set("updater_check", True) + settings.set("updater_errors", 9) + settings.set("updater_last_check", curtime) expected_settings = default_updater_settings() expected_settings["updater_check"] = True @@ -154,7 +155,7 @@ def test_no_update( assert menu_actions_before == menu_actions_after # Check that any previous update errors are cleared. - assert updater.dangerzone.settings.get_updater_settings() == expected_settings + assert settings.get_updater_settings() == expected_settings def test_update_detected( @@ -165,9 +166,9 @@ def test_update_detected( ) -> None: """Test that a newly detected version leads to a notification to the user.""" - qt_updater.dangerzone.settings.set("updater_check", True) - qt_updater.dangerzone.settings.set("updater_last_check", 0) - qt_updater.dangerzone.settings.set("updater_errors", 9) + settings.set("updater_check", True) + settings.set("updater_last_check", 0) + settings.set("updater_errors", 9) # Make requests.get().json() return the following dictionary. mock_upstream_info = {"tag_name": "99.9.9", "body": "changelog"} @@ -197,13 +198,11 @@ def test_update_detected( # Check that the settings have been updated properly. expected_settings = default_updater_settings() expected_settings["updater_check"] = True - expected_settings["updater_last_check"] = qt_updater.dangerzone.settings.get( - "updater_last_check" - ) + expected_settings["updater_last_check"] = settings.get("updater_last_check") expected_settings["updater_latest_version"] = "99.9.9" expected_settings["updater_latest_changelog"] = "

changelog

" expected_settings["updater_errors"] = 0 - assert qt_updater.dangerzone.settings.get_updater_settings() == expected_settings + assert settings.get_updater_settings() == expected_settings # Check that the hamburger icon has changed with the expected SVG image. assert load_svg_spy.call_count == 2 @@ -228,7 +227,7 @@ def test_update_detected( update_dialog_spy = mocker.spy(main_window_module, "UpdateDialog") def check_dialog() -> None: - dialog = qt_updater.dangerzone.app.activeWindow() + dialog = qt_app.activeWindow() update_dialog_spy.assert_called_once() kwargs = update_dialog_spy.call_args.kwargs @@ -274,12 +273,13 @@ def test_update_error( qt_updater: UpdaterThread, monkeypatch: MonkeyPatch, mocker: MockerFixture, + mock_settings: Path, ) -> None: """Test that an error during an update check leads to a notification to the user.""" # Test 1 - Check that the first error does not notify the user. - qt_updater.dangerzone.settings.set("updater_check", True) - qt_updater.dangerzone.settings.set("updater_last_check", 0) - qt_updater.dangerzone.settings.set("updater_errors", 0) + settings.set("updater_check", True) + settings.set("updater_last_check", 0) + settings.set("updater_errors", 0) # Make requests.get() return an errorthe following dictionary. mocker.patch("dangerzone.gui.updater.requests.get") @@ -305,11 +305,9 @@ def test_update_error( # Check that the settings have been updated properly. expected_settings = default_updater_settings() expected_settings["updater_check"] = True - expected_settings["updater_last_check"] = qt_updater.dangerzone.settings.get( - "updater_last_check" - ) + expected_settings["updater_last_check"] = settings.get("updater_last_check") expected_settings["updater_errors"] += 1 - assert qt_updater.dangerzone.settings.get_updater_settings() == expected_settings + assert settings.get_updater_settings() == expected_settings # Check that the hamburger icon has not changed. assert load_svg_spy.call_count == 0 @@ -318,7 +316,7 @@ def test_update_error( assert menu_actions_before == menu_actions_after # Test 2 - Check that the second error does not notify the user either. - qt_updater.dangerzone.settings.set("updater_last_check", 0) + settings.set("updater_last_check", 0) with qtbot.waitSignal(qt_updater.finished): qt_updater.start() @@ -326,16 +324,14 @@ def test_update_error( # Check that the settings have been updated properly. expected_settings["updater_errors"] += 1 - expected_settings["updater_last_check"] = qt_updater.dangerzone.settings.get( - "updater_last_check" - ) - assert qt_updater.dangerzone.settings.get_updater_settings() == expected_settings + expected_settings["updater_last_check"] = settings.get("updater_last_check") + assert settings.get_updater_settings() == expected_settings # Check that no menu entries have been added. assert menu_actions_before == menu_actions_after # Test 3 - Check that a third error shows a new menu entry. - qt_updater.dangerzone.settings.set("updater_last_check", 0) + settings.set("updater_last_check", 0) with qtbot.waitSignal(qt_updater.finished): qt_updater.start() @@ -360,7 +356,7 @@ def test_update_error( update_dialog_spy = mocker.spy(main_window_module, "UpdateDialog") def check_dialog() -> None: - dialog = qt_updater.dangerzone.app.activeWindow() + dialog = qt_app.activeWindow() update_dialog_spy.assert_called_once() kwargs = update_dialog_spy.call_args.kwargs diff --git a/tests/gui/test_updater.py b/tests/gui/test_updater.py index 430a621..f1d3017 100644 --- a/tests/gui/test_updater.py +++ b/tests/gui/test_updater.py @@ -15,7 +15,6 @@ from dangerzone.gui import updater as updater_module from dangerzone.gui.updater import UpdateReport, UpdaterThread from dangerzone.util import get_version -from ..test_settings import default_settings_0_4_1, save_settings from .conftest import generate_isolated_updater @@ -27,7 +26,7 @@ def default_updater_settings() -> dict: """ return { key: val - for key, val in settings.Settings.generate_default_settings().items() + for key, val in settings.generate_default_settings().items() if key.startswith("updater_") } @@ -43,13 +42,15 @@ def test_default_updater_settings(updater: UpdaterThread) -> None: This test is mostly a sanity check. """ - assert ( - updater.dangerzone.settings.get_updater_settings() == default_updater_settings() - ) + assert settings.get_updater_settings() == default_updater_settings() def test_pre_0_4_2_settings( - tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture + tmp_path: Path, + monkeypatch: MonkeyPatch, + mocker: MockerFixture, + mock_settings: Path, + default_settings_0_4_1: dict, ) -> None: """Check settings of installations prior to 0.4.2. @@ -57,11 +58,10 @@ def test_pre_0_4_2_settings( will automatically get the default updater settings, even though they never existed in their settings.json file. """ - save_settings(tmp_path, default_settings_0_4_1()) + settings.SETTINGS = default_settings_0_4_1 + settings.save() updater = generate_isolated_updater(tmp_path, monkeypatch, mocker) - assert ( - updater.dangerzone.settings.get_updater_settings() == default_updater_settings() - ) + assert settings.get_updater_settings() == default_updater_settings() def test_post_0_4_2_settings( @@ -75,9 +75,8 @@ def test_post_0_4_2_settings( erroneously prompted to a version they already have. """ # Store the settings of Dangerzone 0.4.2 to the filesystem. - old_settings = settings.Settings.generate_default_settings() - old_settings["updater_latest_version"] = "0.4.2" - save_settings(tmp_path, old_settings) + settings.SETTINGS = settings.generate_default_settings() + settings.set("updater_latest_version", "0.4.2", autosave=True) # Mimic an upgrade to version 0.4.3, by making Dangerzone report that the current # version is 0.4.3. @@ -89,19 +88,17 @@ def test_post_0_4_2_settings( # Ensure that the Settings class will correct the latest version field to 0.4.3. updater = generate_isolated_updater(tmp_path, monkeypatch, mocker) - assert updater.dangerzone.settings.get_updater_settings() == expected_settings + assert settings.get_updater_settings() == expected_settings # Simulate an updater check that found a newer Dangerzone version (e.g., 0.4.4). expected_settings["updater_latest_version"] = "0.4.4" - updater.dangerzone.settings.set( - "updater_latest_version", expected_settings["updater_latest_version"] - ) - updater.dangerzone.settings.save() + settings.set("updater_latest_version", expected_settings["updater_latest_version"]) + settings.save() # Ensure that the Settings class will leave the "updater_latest_version" field # intact the next time we reload the settings. - updater.dangerzone.settings.load() - assert updater.dangerzone.settings.get_updater_settings() == expected_settings + settings.load() + assert settings.get_updater_settings() == expected_settings @pytest.mark.skipif(platform.system() != "Linux", reason="Linux-only test") @@ -115,7 +112,7 @@ def test_linux_no_check(updater: UpdaterThread, monkeypatch: MonkeyPatch) -> Non monkeypatch.delattr(sys, "dangerzone_dev") assert updater.should_check_for_updates() is False - assert updater.dangerzone.settings.get_updater_settings() == expected_settings + assert settings.get_updater_settings() == expected_settings def test_user_prompts( @@ -130,7 +127,7 @@ def test_user_prompts( expected_settings["updater_check"] = None expected_settings["updater_last_check"] = 0 assert updater.should_check_for_updates() is False - assert updater.dangerzone.settings.get_updater_settings() == expected_settings + assert settings.get_updater_settings() == expected_settings # Second run # @@ -144,14 +141,14 @@ def test_user_prompts( prompt_mock().launch.return_value = False # type: ignore [attr-defined] expected_settings["updater_check"] = False assert updater.should_check_for_updates() is False - assert updater.dangerzone.settings.get_updater_settings() == expected_settings + assert settings.get_updater_settings() == expected_settings # Reset the "updater_check" field and check enabling update checks. - updater.dangerzone.settings.set("updater_check", None) + settings.set("updater_check", None) prompt_mock().launch.return_value = True # type: ignore [attr-defined] expected_settings["updater_check"] = True assert updater.should_check_for_updates() is True - assert updater.dangerzone.settings.get_updater_settings() == expected_settings + assert settings.get_updater_settings() == expected_settings # Third run # @@ -159,7 +156,7 @@ def test_user_prompts( # checks. prompt_mock().side_effect = RuntimeError("Should not be called") # type: ignore [attr-defined] for check in [True, False]: - updater.dangerzone.settings.set("updater_check", check) + settings.set("updater_check", check) assert updater.should_check_for_updates() == check @@ -200,8 +197,8 @@ def test_update_checks( assert_report_equal(report, UpdateReport(error=error_msg)) # Test 4 - Check that cached version/changelog info do not trigger an update check. - updater.dangerzone.settings.set("updater_latest_version", "99.9.9") - updater.dangerzone.settings.set("updater_latest_changelog", "

changelog

") + settings.set("updater_latest_version", "99.9.9") + settings.set("updater_latest_changelog", "

changelog

") report = updater.check_for_updates() assert_report_equal( @@ -211,8 +208,8 @@ def test_update_checks( def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -> None: """Make sure Dangerzone only checks for updates every X hours""" - updater.dangerzone.settings.set("updater_check", True) - updater.dangerzone.settings.set("updater_last_check", 0) + settings.set("updater_check", True) + settings.set("updater_last_check", 0) # Mock some functions before the tests start cooldown_spy = mocker.spy(updater, "_should_postpone_update_check") @@ -233,7 +230,7 @@ def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) - report = updater.check_for_updates() assert cooldown_spy.spy_return is False - assert updater.dangerzone.settings.get("updater_last_check") == curtime + assert settings.get("updater_last_check") == curtime assert_report_equal(report, UpdateReport("99.9.9", "

changelog

")) # Test 2: Advance the current time by 1 second, and ensure that no update will take @@ -242,12 +239,12 @@ def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) - curtime += 1 timestamp_mock.return_value = curtime requests_mock.side_effect = Exception("failed") # type: ignore [attr-defined] - updater.dangerzone.settings.set("updater_latest_version", get_version()) - updater.dangerzone.settings.set("updater_latest_changelog", None) + settings.set("updater_latest_version", get_version()) + settings.set("updater_latest_changelog", None) report = updater.check_for_updates() assert cooldown_spy.spy_return is True - assert updater.dangerzone.settings.get("updater_last_check") == curtime - 1 # type: ignore [unreachable] + assert settings.get("updater_last_check") == curtime - 1 # type: ignore [unreachable] assert_report_equal(report, UpdateReport()) # Test 3: Advance the current time by seconds. Ensure that @@ -258,14 +255,14 @@ def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) - report = updater.check_for_updates() assert cooldown_spy.spy_return is False - assert updater.dangerzone.settings.get("updater_last_check") == curtime + assert settings.get("updater_last_check") == curtime assert_report_equal(report, UpdateReport("99.9.9", "

changelog

")) # Test 4: Make Dangerzone check for updates again, but this time, it should # encounter an error while doing so. In that case, the last check timestamp # should be bumped, so that subsequent checks don't take place. - updater.dangerzone.settings.set("updater_latest_version", get_version()) - updater.dangerzone.settings.set("updater_latest_changelog", None) + settings.set("updater_latest_version", get_version()) + settings.set("updater_latest_changelog", None) curtime += updater_module.UPDATE_CHECK_COOLDOWN_SECS timestamp_mock.return_value = curtime @@ -273,7 +270,7 @@ def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) - report = updater.check_for_updates() assert cooldown_spy.spy_return is False - assert updater.dangerzone.settings.get("updater_last_check") == curtime + assert settings.get("updater_last_check") == curtime error_msg = ( f"Encountered an exception while checking {updater.GH_RELEASE_URL}: failed" ) @@ -375,7 +372,7 @@ def test_update_check_prompt( ) -> None: """Test that the prompt to enable update checks works properly.""" # Force Dangerzone to check immediately for updates - qt_updater.dangerzone.settings.set("updater_last_check", 0) + qt_settings.set("updater_last_check", 0) # Test 1 - Check that on the second run of Dangerzone, the user is prompted to # choose if they want to enable update checks. diff --git a/tests/test_settings.py b/tests/test_settings.py index 87e20ec..16689e6 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -5,50 +5,33 @@ from unittest.mock import PropertyMock import pytest from pytest_mock import MockerFixture -from dangerzone.settings import SETTINGS_FILENAME, Settings - - -def default_settings_0_4_1() -> dict: - """Get the default settings for the 0.4.1 Dangerzone release.""" - return { - "save": True, - "archive": True, - "ocr": True, - "ocr_language": "English", - "open": True, - "open_app": None, - "safe_extension": "-safe.pdf", - } +from dangerzone import settings def test_no_settings_file_creates_new_one( - tmp_path: Path, - mocker: MockerFixture, + mock_settings: Path, ) -> None: """Default settings file is created on first run""" - mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path) - settings = Settings() + settings.load() + assert settings.FILENAME.is_file() - assert settings.settings_filename.is_file() - with settings.settings_filename.open() as settings_file: + with settings.FILENAME.open() as settings_file: new_settings_dict = json.load(settings_file) assert sorted(new_settings_dict.items()) == sorted( settings.generate_default_settings().items() ) -def test_corrupt_settings(tmp_path: Path, mocker: MockerFixture) -> None: +def test_corrupt_settings(mocker: MockerFixture, mock_settings: Path) -> None: # Set some broken settings file corrupt_settings_dict = "{:}" - with (tmp_path / SETTINGS_FILENAME).open("w") as settings_file: + with settings.FILENAME.open("w") as settings_file: settings_file.write(corrupt_settings_dict) - mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path) - settings = Settings() - assert settings.settings_filename.is_file() - + assert settings.FILENAME.is_file() + settings.load() # Check if settings file was reset to the default - new_settings_dict = json.load(open(settings.settings_filename)) + new_settings_dict = json.load(open(settings.FILENAME)) assert new_settings_dict != corrupt_settings_dict assert sorted(new_settings_dict.items()) == sorted( settings.generate_default_settings().items() @@ -56,32 +39,28 @@ def test_corrupt_settings(tmp_path: Path, mocker: MockerFixture) -> None: def test_new_default_setting(tmp_path: Path, mocker: MockerFixture) -> None: - settings = Settings() + mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path) settings.save() # Ensure new default setting is imported into settings mocker.patch( - "dangerzone.settings.Settings.generate_default_settings", + "dangerzone.settings.generate_default_settings", return_value={"mock_setting": 1}, ) - - settings2 = Settings() - assert settings2.get("mock_setting") == 1 + settings.load() + assert settings.get("mock_setting") == 1 def test_new_settings_added(tmp_path: Path, mocker: MockerFixture) -> None: - settings = Settings() - + mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path) # Add new setting settings.set("new_setting_autosaved", 20, autosave=True) settings.set( "new_setting", 10 ) # XXX has to be afterwards; otherwise this will be saved - # Simulate new app startup (settings recreation) - settings2 = Settings() - + settings.load() # Check if new setting persisted - assert 20 == settings2.get("new_setting_autosaved") + assert 20 == settings.get("new_setting_autosaved") with pytest.raises(KeyError): - settings2.get("new_setting") + settings.get("new_setting")