diff --git a/novelwriter/tools/dictionaries.py b/novelwriter/tools/dictionaries.py
index 4edfb4aa..847925a0 100644
--- a/novelwriter/tools/dictionaries.py
+++ b/novelwriter/tools/dictionaries.py
@@ -39,6 +39,7 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED
from novelwriter.common import formatInt, getFileSize
+from novelwriter.error import formatException
logger = logging.getLogger(__name__)
@@ -156,7 +157,7 @@ class GuiDictionaries(QDialog):
i.stem for i in hunspell.iterdir() if i.is_file() and i.suffix == ".aff"
)
self._appendLog(self.tr(
- "{0} additional dictionaries currently installed"
+ "Additional dictionaries found: {0}"
).format(len(self._currDicts)))
qApp.processEvents()
@@ -196,29 +197,21 @@ class GuiDictionaries(QDialog):
@pyqtSlot()
def _doImportHunspell(self):
"""Import a hunspell dictionary from .sox or .oxt file."""
- temp = self.huInput.text()
- output = self._installPath
- if not output:
- return
- if output and temp and (path := Path(temp)).is_file():
- hunspell = output / "hunspell"
- hunspell.mkdir(exist_ok=True)
- try:
- with ZipFile(path, mode="r") as zipObj:
- for item in zipObj.namelist():
- zPath = Path(item)
- if zPath.suffix in (".aff", ".dic"):
- with zipObj.open(item) as zF:
- oPath = hunspell / zPath.name
- oPath.write_bytes(zF.read())
- size = getFileSize(oPath)
- self._appendLog(self.tr(
- "Added: {0} [{1}B]"
- ).format(zPath.name, formatInt(size)))
- except Exception as exc:
- SHARED.error(self.tr("Could not process dictionary file."), exc=exc)
- else:
- SHARED.error(self.tr("File not found."))
+ procErr = self.tr("Could not process dictionary file")
+ if self._installPath:
+ temp = self.huInput.text()
+ if temp and (path := Path(temp)).is_file():
+ hunspell = self._installPath / "hunspell"
+ hunspell.mkdir(exist_ok=True)
+ try:
+ nAff, nDic = self._extractDicts(path, hunspell)
+ if nAff == 0 or nDic == 0:
+ self._appendLog(procErr, err=True)
+ except Exception as exc:
+ self._appendLog(procErr, err=True)
+ self._appendLog(formatException(exc), err=True)
+ else:
+ self._appendLog(procErr, err=True)
return
@pyqtSlot()
@@ -243,13 +236,40 @@ class GuiDictionaries(QDialog):
# Internal Functions
##
- def _appendLog(self, text: str) -> None:
+ def _extractDicts(self, path: Path, output: Path) -> tuple[int, int]:
+ """Extract a zip archive and return the number of .aff and .dic
+ files found in it.
+ """
+ nAff = nDic = 0
+ with ZipFile(path, mode="r") as zipObj:
+ for item in zipObj.namelist():
+ zPath = Path(item)
+ if zPath.suffix not in (".aff", ".dic"):
+ continue
+ nAff += 1 if zPath.suffix == ".aff" else 0
+ nDic += 1 if zPath.suffix == ".dic" else 0
+ with zipObj.open(item) as zF:
+ oPath = output / zPath.name
+ oPath.write_bytes(zF.read())
+ size = getFileSize(oPath)
+ self._appendLog(self.tr(
+ "Added: {0} [{1}B]"
+ ).format(zPath.name, formatInt(size)))
+ return nAff, nDic
+
+ def _appendLog(self, text: str, err: bool = False) -> None:
"""Append a line to the log output."""
- self.infoBox.moveCursor(QTextCursor.MoveOperation.End)
- if self.infoBox.textCursor().position() > 0:
- self.infoBox.insertPlainText("\n")
- self.infoBox.insertPlainText(text)
- self.infoBox.moveCursor(QTextCursor.MoveOperation.End)
+ cursor = self.infoBox.textCursor()
+ cursor.movePosition(QTextCursor.MoveOperation.End)
+ if cursor.position() > 0:
+ cursor.insertText("\n")
+ if err:
+ cursor.insertHtml(f"{text}")
+ else:
+ cursor.insertText(text)
+ cursor.movePosition(QTextCursor.MoveOperation.End)
+ cursor.deleteChar()
+ self.infoBox.setTextCursor(cursor)
return
# END Class GuiDictionaries
diff --git a/tests/test_tools/test_tools_dictionaries.py b/tests/test_tools/test_tools_dictionaries.py
new file mode 100644
index 00000000..5c3b471e
--- /dev/null
+++ b/tests/test_tools/test_tools_dictionaries.py
@@ -0,0 +1,159 @@
+"""
+novelWriter – Dictionary Downloader Tester
+==========================================
+
+This file is a part of novelWriter
+Copyright 2018–2023, Veronica Berglyd Olsen
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+
+import pytest
+import enchant
+
+from zipfile import ZipFile
+
+from tools import getGuiItem
+from mocked import causeException
+
+from PyQt5.QtGui import QDesktopServices
+from PyQt5.QtWidgets import QFileDialog
+
+from novelwriter import SHARED
+from novelwriter.tools.dictionaries import GuiDictionaries
+
+
+@pytest.mark.gui
+def testToolDictionaries_Main(qtbot, monkeypatch, nwGUI, fncPath):
+ """Test the Dictionaries downloader tool."""
+ monkeypatch.setattr(enchant, "get_user_config_dir", lambda *a: str(fncPath))
+
+ # Fail to open
+ with monkeypatch.context() as mp:
+ mp.setattr(enchant, "get_user_config_dir", lambda *a: causeException)
+ nwGUI.showDictionariesDialog()
+ assert SHARED.alert is not None
+ assert SHARED.alert.logMessage == "Could not initialise the dialog."
+
+ # Open the tool
+ nwGUI.showDictionariesDialog()
+ qtbot.waitUntil(lambda: getGuiItem("GuiDictionaries") is not None, timeout=1000)
+
+ nwDicts = getGuiItem("GuiDictionaries")
+ assert isinstance(nwDicts, GuiDictionaries)
+ assert nwDicts.isVisible()
+ assert nwDicts.inPath.text() == str(fncPath)
+
+ # Allow Open Dir
+ SHARED._alert = None
+ with monkeypatch.context() as mp:
+ mp.setattr(QDesktopServices, "openUrl", lambda *a: None)
+ nwDicts._doOpenInstallLocation()
+ assert SHARED.alert is None
+
+ # Fail Open Dir
+ nwDicts.inPath.setText("/foo/bar")
+ nwDicts._doOpenInstallLocation()
+ assert SHARED.alert is not None
+ assert SHARED.alert.logMessage == "Path not found."
+ nwDicts.inPath.setText(str(fncPath))
+
+ # Create Mock Dicts
+ foDict = fncPath / "freeoffice.sox"
+ with ZipFile(foDict, mode="w") as zipObj:
+ zipObj.writestr("en_GB.aff", "foobar")
+ zipObj.writestr("en_GB.dic", "foobar")
+ zipObj.writestr("README.txt", "foobar")
+
+ loDict = fncPath / "libreoffice.oxt"
+ with ZipFile(loDict, mode="w") as zipObj:
+ zipObj.writestr("en_US/en_US.aff", "foobar")
+ zipObj.writestr("en_US/en_US.dic", "foobar")
+ zipObj.writestr("README.txt", "foobar")
+
+ emDict = fncPath / "empty.oxt"
+ with ZipFile(emDict, mode="w") as zipObj:
+ zipObj.writestr("README.txt", "foobar")
+
+ noFile = fncPath / "foobar.oxt"
+ noDict = fncPath / "foobar.sox"
+ noDict.write_bytes(b"foobar")
+
+ assert nwDicts.infoBox.toPlainText().splitlines()[-1] == (
+ "Additional dictionaries found: 0"
+ )
+
+ # Import Free Office Dictionary
+ with monkeypatch.context() as mp:
+ mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(foDict), ""))
+ nwDicts._doBrowseHunspell()
+ assert nwDicts.huInput.text() == str(foDict)
+ nwDicts._doImportHunspell()
+ assert (fncPath / "hunspell").is_dir()
+ assert (fncPath / "hunspell" / "en_GB.aff").is_file()
+ assert (fncPath / "hunspell" / "en_GB.dic").is_file()
+ assert nwDicts.infoBox.blockCount() == 3
+
+ # Import Libre Office Dictionary
+ with monkeypatch.context() as mp:
+ mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(loDict), ""))
+ nwDicts._doBrowseHunspell()
+ assert nwDicts.huInput.text() == str(loDict)
+ nwDicts._doImportHunspell()
+ assert (fncPath / "hunspell").is_dir()
+ assert (fncPath / "hunspell" / "en_US.aff").is_file()
+ assert (fncPath / "hunspell" / "en_US.dic").is_file()
+ assert nwDicts.infoBox.blockCount() == 5
+
+ # Handle Unreadable File
+ with monkeypatch.context() as mp:
+ mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(noDict), ""))
+ nwDicts._doBrowseHunspell()
+ assert nwDicts.huInput.text() == str(noDict)
+ nwDicts._doImportHunspell()
+ assert nwDicts.infoBox.blockCount() == 7
+
+ # Handle File w/No Dicts
+ with monkeypatch.context() as mp:
+ mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(emDict), ""))
+ nwDicts._doBrowseHunspell()
+ assert nwDicts.huInput.text() == str(emDict)
+ nwDicts._doImportHunspell()
+ assert nwDicts.infoBox.blockCount() == 8
+ assert nwDicts.infoBox.toPlainText().splitlines()[-1] == (
+ "Could not process dictionary file"
+ )
+
+ # Handle Non-Existing File
+ with monkeypatch.context() as mp:
+ mp.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(noFile), ""))
+ nwDicts._doBrowseHunspell()
+ nwDicts._doImportHunspell()
+ assert nwDicts.infoBox.blockCount() == 9
+ assert nwDicts.infoBox.toPlainText().splitlines()[-1] == (
+ "Could not process dictionary file"
+ )
+
+ # Re-init
+ nwDicts.initDialog()
+ assert nwDicts.infoBox.blockCount() == 10
+ assert nwDicts.infoBox.toPlainText().splitlines()[-1] == (
+ "Additional dictionaries found: 2"
+ )
+
+ # Close
+ nwDicts._doClose()
+ # qtbot.stop()
+
+# END Test testToolDictionaries_Main