make dangerzone.settings a global

This commit is contained in:
Alexis Métaireau 2025-03-24 15:42:59 +01:00
parent 1353fa76d9
commit 8142e4a48a
No known key found for this signature in database
GPG key ID: C65C7A89A8FFC56E
10 changed files with 215 additions and 241 deletions

View file

@ -12,6 +12,8 @@ from typing import Optional
from colorama import Fore from colorama import Fore
from .. import settings
# FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details. # FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details.
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from PySide2 import QtCore, QtGui, QtWidgets from PySide2 import QtCore, QtGui, QtWidgets
@ -80,7 +82,7 @@ class DangerzoneGui(DangerzoneCore):
elif platform.system() == "Linux": elif platform.system() == "Linux":
# Get the PDF reader command # 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 # %f, %F, %u, and %U are filenames or URLS -- so replace with the file to open
for i in range(len(args)): for i in range(len(args)):
if ( if (

View file

@ -8,6 +8,8 @@ from multiprocessing.pool import ThreadPool
from pathlib import Path from pathlib import Path
from typing import List, Optional from typing import List, Optional
from .. import settings
# FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details. # FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details.
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets 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 = hamburger_menu.addAction("Check for updates")
self.toggle_updates_action.triggered.connect(self.toggle_updates_triggered) self.toggle_updates_action.triggered.connect(self.toggle_updates_triggered)
self.toggle_updates_action.setCheckable(True) self.toggle_updates_action.setCheckable(True)
self.toggle_updates_action.setChecked( self.toggle_updates_action.setChecked(bool(settings.get("updater_check")))
bool(self.dangerzone.settings.get("updater_check"))
)
# Add the "Exit" action # Add the "Exit" action
hamburger_menu.addSeparator() hamburger_menu.addSeparator()
@ -231,8 +231,8 @@ class MainWindow(QtWidgets.QMainWindow):
def show_update_success(self) -> None: def show_update_success(self) -> None:
"""Inform the user about a new Dangerzone release.""" """Inform the user about a new Dangerzone release."""
version = self.dangerzone.settings.get("updater_latest_version") version = settings.get("updater_latest_version")
changelog = self.dangerzone.settings.get("updater_latest_changelog") changelog = settings.get("updater_latest_changelog")
changelog_widget = CollapsibleBox("What's New?") changelog_widget = CollapsibleBox("What's New?")
changelog_layout = QtWidgets.QVBoxLayout() changelog_layout = QtWidgets.QVBoxLayout()
@ -277,8 +277,8 @@ class MainWindow(QtWidgets.QMainWindow):
def toggle_updates_triggered(self) -> None: def toggle_updates_triggered(self) -> None:
"""Change the underlying update check settings based on the user's choice.""" """Change the underlying update check settings based on the user's choice."""
check = self.toggle_updates_action.isChecked() check = self.toggle_updates_action.isChecked()
self.dangerzone.settings.set("updater_check", check) settings.set("updater_check", check)
self.dangerzone.settings.save() settings.save()
def handle_docker_desktop_version_check( def handle_docker_desktop_version_check(
self, is_version_valid: bool, version: str 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 there are no new updates, reset the error counter (if any) and return.
if report.empty(): if report.empty():
self.dangerzone.settings.set("updater_errors", 0, autosave=True) settings.set("updater_errors", 0, autosave=True)
return return
hamburger_menu = self.hamburger_button.menu() hamburger_menu = self.hamburger_button.menu()
if report.error: if report.error:
log.error(f"Encountered an error during an update check: {report.error}") log.error(f"Encountered an error during an update check: {report.error}")
errors = self.dangerzone.settings.get("updater_errors") + 1 errors = settings.get("updater_errors") + 1
self.dangerzone.settings.set("updater_errors", errors) settings.set("updater_errors", errors)
self.dangerzone.settings.save() settings.save()
self.updater_error = report.error self.updater_error = report.error
# If we encounter more than three errors in a row, show a red notification # 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) hamburger_menu.insertAction(sep, error_action)
else: else:
log.debug(f"Handling new version: {report.version}") log.debug(f"Handling new version: {report.version}")
self.dangerzone.settings.set("updater_latest_version", report.version) settings.set("updater_latest_version", report.version)
self.dangerzone.settings.set("updater_latest_changelog", report.changelog) settings.set("updater_latest_changelog", report.changelog)
self.dangerzone.settings.set("updater_errors", 0) settings.set("updater_errors", 0)
# FIXME: Save the settings to the filesystem only when they have really changed, # FIXME: Save the settings to the filesystem only when they have really changed,
# maybe with a dirty bit. # maybe with a dirty bit.
self.dangerzone.settings.save() settings.save()
self.hamburger_button.setIcon( self.hamburger_button.setIcon(
QtGui.QIcon( QtGui.QIcon(
@ -985,39 +985,37 @@ class SettingsWidget(QtWidgets.QWidget):
self.setLayout(layout) self.setLayout(layout)
# Load values from settings # Load values from settings
if self.dangerzone.settings.get("save"): if settings.get("save"):
self.save_checkbox.setCheckState(QtCore.Qt.Checked) self.save_checkbox.setCheckState(QtCore.Qt.Checked)
else: else:
self.save_checkbox.setCheckState(QtCore.Qt.Unchecked) self.save_checkbox.setCheckState(QtCore.Qt.Unchecked)
if self.dangerzone.settings.get("safe_extension"): if settings.get("safe_extension"):
self.safe_extension.setText(self.dangerzone.settings.get("safe_extension")) self.safe_extension.setText(settings.get("safe_extension"))
else: else:
self.safe_extension.setText(SAFE_EXTENSION) self.safe_extension.setText(SAFE_EXTENSION)
if self.dangerzone.settings.get("archive"): if settings.get("archive"):
self.radio_move_untrusted.setChecked(True) self.radio_move_untrusted.setChecked(True)
else: else:
self.radio_save_to.setChecked(True) self.radio_save_to.setChecked(True)
if self.dangerzone.settings.get("ocr"): if settings.get("ocr"):
self.ocr_checkbox.setCheckState(QtCore.Qt.Checked) self.ocr_checkbox.setCheckState(QtCore.Qt.Checked)
else: else:
self.ocr_checkbox.setCheckState(QtCore.Qt.Unchecked) 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: if index != -1:
self.ocr_combobox.setCurrentIndex(index) self.ocr_combobox.setCurrentIndex(index)
if self.dangerzone.settings.get("open"): if settings.get("open"):
self.open_checkbox.setCheckState(QtCore.Qt.Checked) self.open_checkbox.setCheckState(QtCore.Qt.Checked)
else: else:
self.open_checkbox.setCheckState(QtCore.Qt.Unchecked) self.open_checkbox.setCheckState(QtCore.Qt.Unchecked)
if platform.system() == "Linux": if platform.system() == "Linux":
index = self.open_combobox.findText( index = self.open_combobox.findText(settings.get("open_app"))
self.dangerzone.settings.get("open_app")
)
if index != -1: if index != -1:
self.open_combobox.setCurrentIndex(index) self.open_combobox.setCurrentIndex(index)
@ -1138,21 +1136,15 @@ class SettingsWidget(QtWidgets.QWidget):
document.output_filename = tmp document.output_filename = tmp
# Update settings # Update settings
self.dangerzone.settings.set( settings.set("save", self.save_checkbox.checkState() == QtCore.Qt.Checked)
"save", self.save_checkbox.checkState() == QtCore.Qt.Checked settings.set("safe_extension", self.safe_extension.text())
) settings.set("archive", self.radio_move_untrusted.isChecked())
self.dangerzone.settings.set("safe_extension", self.safe_extension.text()) settings.set("ocr", self.ocr_checkbox.checkState() == QtCore.Qt.Checked)
self.dangerzone.settings.set("archive", self.radio_move_untrusted.isChecked()) settings.set("ocr_language", self.ocr_combobox.currentText())
self.dangerzone.settings.set( settings.set("open", self.open_checkbox.checkState() == QtCore.Qt.Checked)
"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
)
if platform.system() == "Linux": if platform.system() == "Linux":
self.dangerzone.settings.set("open_app", self.open_combobox.currentText()) settings.set("open_app", self.open_combobox.currentText())
self.dangerzone.settings.save() settings.save()
# Start! # Start!
self.start_clicked.emit() self.start_clicked.emit()
@ -1234,10 +1226,8 @@ class DocumentsListWidget(QtWidgets.QListWidget):
def get_ocr_lang(self) -> Optional[str]: def get_ocr_lang(self) -> Optional[str]:
ocr_lang = None ocr_lang = None
if self.dangerzone.settings.get("ocr"): if settings.get("ocr"):
ocr_lang = self.dangerzone.ocr_languages[ ocr_lang = self.dangerzone.ocr_languages[settings.get("ocr_language")]
self.dangerzone.settings.get("ocr_language")
]
return ocr_lang return ocr_lang
@ -1326,7 +1316,7 @@ class DocumentWidget(QtWidgets.QWidget):
return return
# Open # Open
if self.dangerzone.settings.get("open"): if settings.get("open"):
self.dangerzone.open_pdf_viewer(self.document.output_filename) self.dangerzone.open_pdf_viewer(self.document.output_filename)

View file

@ -10,6 +10,8 @@ from typing import Optional
from packaging import version from packaging import version
from .. import settings
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from PySide2 import QtCore, QtWidgets from PySide2 import QtCore, QtWidgets
else: else:
@ -126,11 +128,11 @@ class UpdaterThread(QtCore.QThread):
@property @property
def check(self) -> Optional[bool]: def check(self) -> Optional[bool]:
return self.dangerzone.settings.get("updater_check") return settings.get("updater_check")
@check.setter @check.setter
def check(self, val: bool) -> None: 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]: def prompt_for_checks(self) -> Optional[bool]:
"""Ask the user if they want to be informed about Dangerzone updates.""" """Ask the user if they want to be informed about Dangerzone updates."""
@ -169,9 +171,9 @@ class UpdaterThread(QtCore.QThread):
return False return False
log.debug("Checking if first run of Dangerzone") 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") 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 return False
log.debug("Checking if user has already expressed their preference") log.debug("Checking if user has already expressed their preference")
@ -204,7 +206,7 @@ class UpdaterThread(QtCore.QThread):
again. again.
""" """
current_time = self._get_now_timestamp() 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: if current_time < last_check + UPDATE_CHECK_COOLDOWN_SECS:
log.debug("Cooling down update checks") log.debug("Cooling down update checks")
return True return True
@ -256,12 +258,12 @@ class UpdaterThread(QtCore.QThread):
2. In GitHub, by hitting the latest releases API. 2. In GitHub, by hitting the latest releases API.
""" """
log.debug("Checking for Dangerzone updates") 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): if version.parse(get_version()) < version.parse(latest_version):
log.debug("Determined that there is an update due to cached results") log.debug("Determined that there is an update due to cached results")
return UpdateReport( return UpdateReport(
version=latest_version, 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 # 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(): if self._should_postpone_update_check():
return UpdateReport() return UpdateReport()
else: else:
self.dangerzone.settings.set( settings.set("updater_last_check", self._get_now_timestamp(), autosave=True)
"updater_last_check", self._get_now_timestamp(), autosave=True
)
log.debug("Checking the latest GitHub release") log.debug("Checking the latest GitHub release")
report = self.get_latest_info() report = self.get_latest_info()

View file

@ -5,10 +5,9 @@ from typing import Callable, List, Optional
import colorama import colorama
from . import errors, util from . import errors, settings, util
from .document import Document from .document import Document
from .isolation_provider.base import IsolationProvider from .isolation_provider.base import IsolationProvider
from .settings import Settings
from .util import get_resource_path from .util import get_resource_path
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -28,8 +27,6 @@ class DangerzoneCore(object):
unsorted_ocr_languages = json.load(f) unsorted_ocr_languages = json.load(f)
self.ocr_languages = dict(sorted(unsorted_ocr_languages.items())) self.ocr_languages = dict(sorted(unsorted_ocr_languages.items()))
# Load settings
self.settings = Settings()
self.documents: List[Document] = [] self.documents: List[Document] = []
self.isolation_provider = isolation_provider self.isolation_provider = isolation_provider

View file

@ -10,19 +10,12 @@ from .util import get_config_dir, get_version
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
SETTINGS_FILENAME: str = "settings.json" FILENAME = get_config_dir() / "settings.json"
SETTINGS: dict = {}
class Settings: def generate_default_settings() -> Dict[str, Any]:
settings: Dict[str, Any]
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 { return {
"save": True, "save": True,
"archive": True, "archive": True,
@ -39,52 +32,56 @@ class Settings:
"updater_errors": 0, "updater_errors": 0,
} }
def get(self, key: str) -> Any:
return self.settings[key]
def set(self, key: str, val: Any, autosave: bool = False) -> None: def get(key: str) -> Any:
return SETTINGS[key]
def set(key: str, val: Any, autosave: bool = False) -> None:
global SETTINGS
try: try:
old_val = self.get(key) old_val = SETTINGS.get(key)
except KeyError: except KeyError:
old_val = None old_val = None
self.settings[key] = val SETTINGS[key] = val
if autosave and val != old_val: if autosave and val != old_val:
self.save() save()
def get_updater_settings(self) -> Dict[str, Any]:
return {
key: val for key, val in self.settings.items() if key.startswith("updater_")
}
def load(self) -> None: def get_updater_settings() -> Dict[str, Any]:
if os.path.isfile(self.settings_filename): return {key: val for key, val in SETTINGS.items() if key.startswith("updater_")}
self.settings = self.default_settings
def load() -> None:
global SETTINGS
default_settings = generate_default_settings()
if FILENAME.is_file() and FILENAME.exists():
# If the settings file exists, load it # If the settings file exists, load it
try: try:
with open(self.settings_filename, "r") as settings_file: with FILENAME.open("r") as settings_file:
self.settings = json.load(settings_file) SETTINGS = json.load(settings_file)
# If it's missing any fields, add them from the default settings # If it's missing any fields, add them from the default settings
for key in self.default_settings: for key in default_settings:
if key not in self.settings: if key not in SETTINGS:
self.settings[key] = self.default_settings[key] SETTINGS[key] = default_settings[key]
elif key == "updater_latest_version": elif key == "updater_latest_version":
if version.parse(get_version()) > version.parse(self.get(key)): if version.parse(get_version()) > version.parse(get(key)):
self.set(key, get_version()) set(key, get_version())
except Exception: except Exception:
log.error("Error loading settings, falling back to default") log.error("Error loading settings, falling back to default")
self.settings = self.default_settings SETTINGS = default_settings
else: else:
# Save with default settings # Save with default settings
log.info("Settings file doesn't exist, starting with default") log.info("Settings file doesn't exist, starting with default")
self.settings = self.default_settings SETTINGS = default_settings
self.save() save()
def save(self) -> None:
self.settings_filename.parent.mkdir(parents=True, exist_ok=True) def save() -> None:
with self.settings_filename.open("w") as settings_file: FILENAME.parent.mkdir(parents=True, exist_ok=True)
json.dump(self.settings, settings_file, indent=4) with FILENAME.open("w") as settings_file:
json.dump(SETTINGS, settings_file, indent=4)

View file

@ -6,6 +6,7 @@ from pathlib import Path
from typing import Callable, List from typing import Callable, List
import pytest import pytest
from pytest_mock import MockerFixture
from dangerzone.document import SAFE_EXTENSION from dangerzone.document import SAFE_EXTENSION
from dangerzone.gui import Application from dangerzone.gui import Application
@ -111,6 +112,26 @@ def sample_pdf() -> str:
return str(test_docs_dir.joinpath(BASIC_SAMPLE_PDF)) 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" SAMPLE_DIRECTORY = "test_docs"
BASIC_SAMPLE_PDF = "sample-pdf.pdf" BASIC_SAMPLE_PDF = "sample-pdf.pdf"
BASIC_SAMPLE_DOC = "sample-doc.doc" BASIC_SAMPLE_DOC = "sample-doc.doc"

View file

@ -30,25 +30,20 @@ def generate_isolated_updater(
else: else:
app = get_qt_app() app = get_qt_app()
dummy = Dummy() dangerzone = DangerzoneGui(app, isolation_provider=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)
updater = UpdaterThread(dangerzone) updater = UpdaterThread(dangerzone)
return updater return updater
@pytest.fixture @pytest.fixture
def updater( def updater(
tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture tmp_path: Path, monkeypatch: MonkeyPatch, mocker: MockerFixture, mock_settings: Path
) -> UpdaterThread: ) -> UpdaterThread:
return generate_isolated_updater(tmp_path, monkeypatch, mocker) return generate_isolated_updater(tmp_path, monkeypatch, mocker)
@pytest.fixture @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) return generate_isolated_updater(tmp_path, monkeypatch)

View file

@ -3,13 +3,14 @@ import pathlib
import platform import platform
import shutil import shutil
import time import time
from pathlib import Path
from typing import List from typing import List
from pytest import MonkeyPatch, fixture from pytest import MonkeyPatch, fixture
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from pytestqt.qtbot import QtBot from pytestqt.qtbot import QtBot
from dangerzone import errors from dangerzone import errors, settings
from dangerzone.document import Document from dangerzone.document import Document
from dangerzone.gui import MainWindow from dangerzone.gui import MainWindow
from dangerzone.gui import main_window as main_window_module from dangerzone.gui import main_window as main_window_module
@ -96,7 +97,7 @@ def test_default_menu(
updater: UpdaterThread, updater: UpdaterThread,
) -> None: ) -> None:
"""Check that the default menu entries are in order.""" """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) window = MainWindow(updater.dangerzone)
menu_actions = window.hamburger_button.menu().actions() menu_actions = window.hamburger_button.menu().actions()
@ -114,7 +115,7 @@ def test_default_menu(
toggle_updates_action.trigger() toggle_updates_action.trigger()
assert not toggle_updates_action.isChecked() 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( 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 # 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. # report is received that does not affect the menu entries.
curtime = int(time.time()) curtime = int(time.time())
updater.dangerzone.settings.set("updater_check", True) settings.set("updater_check", True)
updater.dangerzone.settings.set("updater_errors", 9) settings.set("updater_errors", 9)
updater.dangerzone.settings.set("updater_last_check", curtime) settings.set("updater_last_check", curtime)
expected_settings = default_updater_settings() expected_settings = default_updater_settings()
expected_settings["updater_check"] = True expected_settings["updater_check"] = True
@ -154,7 +155,7 @@ def test_no_update(
assert menu_actions_before == menu_actions_after assert menu_actions_before == menu_actions_after
# Check that any previous update errors are cleared. # 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( def test_update_detected(
@ -165,9 +166,9 @@ def test_update_detected(
) -> None: ) -> None:
"""Test that a newly detected version leads to a notification to the user.""" """Test that a newly detected version leads to a notification to the user."""
qt_updater.dangerzone.settings.set("updater_check", True) settings.set("updater_check", True)
qt_updater.dangerzone.settings.set("updater_last_check", 0) settings.set("updater_last_check", 0)
qt_updater.dangerzone.settings.set("updater_errors", 9) settings.set("updater_errors", 9)
# Make requests.get().json() return the following dictionary. # Make requests.get().json() return the following dictionary.
mock_upstream_info = {"tag_name": "99.9.9", "body": "changelog"} 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. # Check that the settings have been updated properly.
expected_settings = default_updater_settings() expected_settings = default_updater_settings()
expected_settings["updater_check"] = True expected_settings["updater_check"] = True
expected_settings["updater_last_check"] = qt_updater.dangerzone.settings.get( expected_settings["updater_last_check"] = settings.get("updater_last_check")
"updater_last_check"
)
expected_settings["updater_latest_version"] = "99.9.9" expected_settings["updater_latest_version"] = "99.9.9"
expected_settings["updater_latest_changelog"] = "<p>changelog</p>" expected_settings["updater_latest_changelog"] = "<p>changelog</p>"
expected_settings["updater_errors"] = 0 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. # Check that the hamburger icon has changed with the expected SVG image.
assert load_svg_spy.call_count == 2 assert load_svg_spy.call_count == 2
@ -228,7 +227,7 @@ def test_update_detected(
update_dialog_spy = mocker.spy(main_window_module, "UpdateDialog") update_dialog_spy = mocker.spy(main_window_module, "UpdateDialog")
def check_dialog() -> None: def check_dialog() -> None:
dialog = qt_updater.dangerzone.app.activeWindow() dialog = qt_app.activeWindow()
update_dialog_spy.assert_called_once() update_dialog_spy.assert_called_once()
kwargs = update_dialog_spy.call_args.kwargs kwargs = update_dialog_spy.call_args.kwargs
@ -274,12 +273,13 @@ def test_update_error(
qt_updater: UpdaterThread, qt_updater: UpdaterThread,
monkeypatch: MonkeyPatch, monkeypatch: MonkeyPatch,
mocker: MockerFixture, mocker: MockerFixture,
mock_settings: Path,
) -> None: ) -> None:
"""Test that an error during an update check leads to a notification to the user.""" """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. # Test 1 - Check that the first error does not notify the user.
qt_updater.dangerzone.settings.set("updater_check", True) settings.set("updater_check", True)
qt_updater.dangerzone.settings.set("updater_last_check", 0) settings.set("updater_last_check", 0)
qt_updater.dangerzone.settings.set("updater_errors", 0) settings.set("updater_errors", 0)
# Make requests.get() return an errorthe following dictionary. # Make requests.get() return an errorthe following dictionary.
mocker.patch("dangerzone.gui.updater.requests.get") mocker.patch("dangerzone.gui.updater.requests.get")
@ -305,11 +305,9 @@ def test_update_error(
# Check that the settings have been updated properly. # Check that the settings have been updated properly.
expected_settings = default_updater_settings() expected_settings = default_updater_settings()
expected_settings["updater_check"] = True expected_settings["updater_check"] = True
expected_settings["updater_last_check"] = qt_updater.dangerzone.settings.get( expected_settings["updater_last_check"] = settings.get("updater_last_check")
"updater_last_check"
)
expected_settings["updater_errors"] += 1 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. # Check that the hamburger icon has not changed.
assert load_svg_spy.call_count == 0 assert load_svg_spy.call_count == 0
@ -318,7 +316,7 @@ def test_update_error(
assert menu_actions_before == menu_actions_after assert menu_actions_before == menu_actions_after
# Test 2 - Check that the second error does not notify the user either. # 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): with qtbot.waitSignal(qt_updater.finished):
qt_updater.start() qt_updater.start()
@ -326,16 +324,14 @@ def test_update_error(
# Check that the settings have been updated properly. # Check that the settings have been updated properly.
expected_settings["updater_errors"] += 1 expected_settings["updater_errors"] += 1
expected_settings["updater_last_check"] = qt_updater.dangerzone.settings.get( expected_settings["updater_last_check"] = settings.get("updater_last_check")
"updater_last_check" assert settings.get_updater_settings() == expected_settings
)
assert qt_updater.dangerzone.settings.get_updater_settings() == expected_settings
# Check that no menu entries have been added. # Check that no menu entries have been added.
assert menu_actions_before == menu_actions_after assert menu_actions_before == menu_actions_after
# Test 3 - Check that a third error shows a new menu entry. # 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): with qtbot.waitSignal(qt_updater.finished):
qt_updater.start() qt_updater.start()
@ -360,7 +356,7 @@ def test_update_error(
update_dialog_spy = mocker.spy(main_window_module, "UpdateDialog") update_dialog_spy = mocker.spy(main_window_module, "UpdateDialog")
def check_dialog() -> None: def check_dialog() -> None:
dialog = qt_updater.dangerzone.app.activeWindow() dialog = qt_app.activeWindow()
update_dialog_spy.assert_called_once() update_dialog_spy.assert_called_once()
kwargs = update_dialog_spy.call_args.kwargs kwargs = update_dialog_spy.call_args.kwargs

View file

@ -15,7 +15,6 @@ from dangerzone.gui import updater as updater_module
from dangerzone.gui.updater import UpdateReport, UpdaterThread from dangerzone.gui.updater import UpdateReport, UpdaterThread
from dangerzone.util import get_version from dangerzone.util import get_version
from ..test_settings import default_settings_0_4_1, save_settings
from .conftest import generate_isolated_updater from .conftest import generate_isolated_updater
@ -27,7 +26,7 @@ def default_updater_settings() -> dict:
""" """
return { return {
key: val 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_") if key.startswith("updater_")
} }
@ -43,13 +42,15 @@ def test_default_updater_settings(updater: UpdaterThread) -> None:
This test is mostly a sanity check. This test is mostly a sanity check.
""" """
assert ( assert settings.get_updater_settings() == default_updater_settings()
updater.dangerzone.settings.get_updater_settings() == default_updater_settings()
)
def test_pre_0_4_2_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: ) -> None:
"""Check settings of installations prior to 0.4.2. """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 will automatically get the default updater settings, even though they never existed
in their settings.json file. 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) updater = generate_isolated_updater(tmp_path, monkeypatch, mocker)
assert ( assert settings.get_updater_settings() == default_updater_settings()
updater.dangerzone.settings.get_updater_settings() == default_updater_settings()
)
def test_post_0_4_2_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. erroneously prompted to a version they already have.
""" """
# Store the settings of Dangerzone 0.4.2 to the filesystem. # Store the settings of Dangerzone 0.4.2 to the filesystem.
old_settings = settings.Settings.generate_default_settings() settings.SETTINGS = settings.generate_default_settings()
old_settings["updater_latest_version"] = "0.4.2" settings.set("updater_latest_version", "0.4.2", autosave=True)
save_settings(tmp_path, old_settings)
# Mimic an upgrade to version 0.4.3, by making Dangerzone report that the current # Mimic an upgrade to version 0.4.3, by making Dangerzone report that the current
# version is 0.4.3. # 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. # Ensure that the Settings class will correct the latest version field to 0.4.3.
updater = generate_isolated_updater(tmp_path, monkeypatch, mocker) 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). # Simulate an updater check that found a newer Dangerzone version (e.g., 0.4.4).
expected_settings["updater_latest_version"] = "0.4.4" expected_settings["updater_latest_version"] = "0.4.4"
updater.dangerzone.settings.set( settings.set("updater_latest_version", expected_settings["updater_latest_version"])
"updater_latest_version", expected_settings["updater_latest_version"] settings.save()
)
updater.dangerzone.settings.save()
# Ensure that the Settings class will leave the "updater_latest_version" field # Ensure that the Settings class will leave the "updater_latest_version" field
# intact the next time we reload the settings. # intact the next time we reload the settings.
updater.dangerzone.settings.load() settings.load()
assert updater.dangerzone.settings.get_updater_settings() == expected_settings assert settings.get_updater_settings() == expected_settings
@pytest.mark.skipif(platform.system() != "Linux", reason="Linux-only test") @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") monkeypatch.delattr(sys, "dangerzone_dev")
assert updater.should_check_for_updates() is 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
def test_user_prompts( def test_user_prompts(
@ -130,7 +127,7 @@ def test_user_prompts(
expected_settings["updater_check"] = None expected_settings["updater_check"] = None
expected_settings["updater_last_check"] = 0 expected_settings["updater_last_check"] = 0
assert updater.should_check_for_updates() is 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
# Second run # Second run
# #
@ -144,14 +141,14 @@ def test_user_prompts(
prompt_mock().launch.return_value = False # type: ignore [attr-defined] prompt_mock().launch.return_value = False # type: ignore [attr-defined]
expected_settings["updater_check"] = False expected_settings["updater_check"] = False
assert updater.should_check_for_updates() is 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. # 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] prompt_mock().launch.return_value = True # type: ignore [attr-defined]
expected_settings["updater_check"] = True expected_settings["updater_check"] = True
assert updater.should_check_for_updates() is 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 # Third run
# #
@ -159,7 +156,7 @@ def test_user_prompts(
# checks. # checks.
prompt_mock().side_effect = RuntimeError("Should not be called") # type: ignore [attr-defined] prompt_mock().side_effect = RuntimeError("Should not be called") # type: ignore [attr-defined]
for check in [True, False]: for check in [True, False]:
updater.dangerzone.settings.set("updater_check", check) settings.set("updater_check", check)
assert updater.should_check_for_updates() == check assert updater.should_check_for_updates() == check
@ -200,8 +197,8 @@ def test_update_checks(
assert_report_equal(report, UpdateReport(error=error_msg)) assert_report_equal(report, UpdateReport(error=error_msg))
# Test 4 - Check that cached version/changelog info do not trigger an update check. # Test 4 - Check that cached version/changelog info do not trigger an update check.
updater.dangerzone.settings.set("updater_latest_version", "99.9.9") settings.set("updater_latest_version", "99.9.9")
updater.dangerzone.settings.set("updater_latest_changelog", "<p>changelog</p>") settings.set("updater_latest_changelog", "<p>changelog</p>")
report = updater.check_for_updates() report = updater.check_for_updates()
assert_report_equal( assert_report_equal(
@ -211,8 +208,8 @@ def test_update_checks(
def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -> None: def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -> None:
"""Make sure Dangerzone only checks for updates every X hours""" """Make sure Dangerzone only checks for updates every X hours"""
updater.dangerzone.settings.set("updater_check", True) settings.set("updater_check", True)
updater.dangerzone.settings.set("updater_last_check", 0) settings.set("updater_last_check", 0)
# Mock some functions before the tests start # Mock some functions before the tests start
cooldown_spy = mocker.spy(updater, "_should_postpone_update_check") 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() report = updater.check_for_updates()
assert cooldown_spy.spy_return is False 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", "<p>changelog</p>")) assert_report_equal(report, UpdateReport("99.9.9", "<p>changelog</p>"))
# Test 2: Advance the current time by 1 second, and ensure that no update will take # 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 curtime += 1
timestamp_mock.return_value = curtime timestamp_mock.return_value = curtime
requests_mock.side_effect = Exception("failed") # type: ignore [attr-defined] requests_mock.side_effect = Exception("failed") # type: ignore [attr-defined]
updater.dangerzone.settings.set("updater_latest_version", get_version()) settings.set("updater_latest_version", get_version())
updater.dangerzone.settings.set("updater_latest_changelog", None) settings.set("updater_latest_changelog", None)
report = updater.check_for_updates() report = updater.check_for_updates()
assert cooldown_spy.spy_return is True 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()) assert_report_equal(report, UpdateReport())
# Test 3: Advance the current time by <cooldown period> seconds. Ensure that # Test 3: Advance the current time by <cooldown period> seconds. Ensure that
@ -258,14 +255,14 @@ def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -
report = updater.check_for_updates() report = updater.check_for_updates()
assert cooldown_spy.spy_return is False 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", "<p>changelog</p>")) assert_report_equal(report, UpdateReport("99.9.9", "<p>changelog</p>"))
# Test 4: Make Dangerzone check for updates again, but this time, it should # 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 # encounter an error while doing so. In that case, the last check timestamp
# should be bumped, so that subsequent checks don't take place. # should be bumped, so that subsequent checks don't take place.
updater.dangerzone.settings.set("updater_latest_version", get_version()) settings.set("updater_latest_version", get_version())
updater.dangerzone.settings.set("updater_latest_changelog", None) settings.set("updater_latest_changelog", None)
curtime += updater_module.UPDATE_CHECK_COOLDOWN_SECS curtime += updater_module.UPDATE_CHECK_COOLDOWN_SECS
timestamp_mock.return_value = curtime timestamp_mock.return_value = curtime
@ -273,7 +270,7 @@ def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -
report = updater.check_for_updates() report = updater.check_for_updates()
assert cooldown_spy.spy_return is False 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 = ( error_msg = (
f"Encountered an exception while checking {updater.GH_RELEASE_URL}: failed" f"Encountered an exception while checking {updater.GH_RELEASE_URL}: failed"
) )
@ -375,7 +372,7 @@ def test_update_check_prompt(
) -> None: ) -> None:
"""Test that the prompt to enable update checks works properly.""" """Test that the prompt to enable update checks works properly."""
# Force Dangerzone to check immediately for updates # 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 # Test 1 - Check that on the second run of Dangerzone, the user is prompted to
# choose if they want to enable update checks. # choose if they want to enable update checks.

View file

@ -5,50 +5,33 @@ from unittest.mock import PropertyMock
import pytest import pytest
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from dangerzone.settings import SETTINGS_FILENAME, Settings from dangerzone import 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",
}
def test_no_settings_file_creates_new_one( def test_no_settings_file_creates_new_one(
tmp_path: Path, mock_settings: Path,
mocker: MockerFixture,
) -> None: ) -> None:
"""Default settings file is created on first run""" """Default settings file is created on first run"""
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path) settings.load()
settings = Settings() assert settings.FILENAME.is_file()
assert settings.settings_filename.is_file() with settings.FILENAME.open() as settings_file:
with settings.settings_filename.open() as settings_file:
new_settings_dict = json.load(settings_file) new_settings_dict = json.load(settings_file)
assert sorted(new_settings_dict.items()) == sorted( assert sorted(new_settings_dict.items()) == sorted(
settings.generate_default_settings().items() 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 # Set some broken settings file
corrupt_settings_dict = "{:}" 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) settings_file.write(corrupt_settings_dict)
mocker.patch("dangerzone.settings.get_config_dir", return_value=tmp_path) assert settings.FILENAME.is_file()
settings = Settings() settings.load()
assert settings.settings_filename.is_file()
# Check if settings file was reset to the default # 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 new_settings_dict != corrupt_settings_dict
assert sorted(new_settings_dict.items()) == sorted( assert sorted(new_settings_dict.items()) == sorted(
settings.generate_default_settings().items() 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: 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() settings.save()
# Ensure new default setting is imported into settings # Ensure new default setting is imported into settings
mocker.patch( mocker.patch(
"dangerzone.settings.Settings.generate_default_settings", "dangerzone.settings.generate_default_settings",
return_value={"mock_setting": 1}, return_value={"mock_setting": 1},
) )
settings.load()
settings2 = Settings() assert settings.get("mock_setting") == 1
assert settings2.get("mock_setting") == 1
def test_new_settings_added(tmp_path: Path, mocker: MockerFixture) -> None: 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 # Add new setting
settings.set("new_setting_autosaved", 20, autosave=True) settings.set("new_setting_autosaved", 20, autosave=True)
settings.set( settings.set(
"new_setting", 10 "new_setting", 10
) # XXX has to be afterwards; otherwise this will be saved ) # XXX has to be afterwards; otherwise this will be saved
# Simulate new app startup (settings recreation) settings.load()
settings2 = Settings()
# Check if new setting persisted # Check if new setting persisted
assert 20 == settings2.get("new_setting_autosaved") assert 20 == settings.get("new_setting_autosaved")
with pytest.raises(KeyError): with pytest.raises(KeyError):
settings2.get("new_setting") settings.get("new_setting")