diff --git a/novelwriter/common.py b/novelwriter/common.py index f914a6d1b..2627a1b09 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -487,6 +487,14 @@ def makeFileNameSafe(text: str) -> str: return "".join(c for c in text if c.isalnum() or c in allowed) +def getFileSize(path: Path) -> int: + """Return the size of a file.""" + try: + return path.stat().st_size + except Exception: + return -1 + + # =============================================================================================== # # Other Functions # =============================================================================================== # diff --git a/novelwriter/config.py b/novelwriter/config.py index ba422efae..390353714 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -225,6 +225,7 @@ def __init__(self) -> None: # Other System Info self.hostName = QSysInfo.machineHostName() self.kernelVer = QSysInfo.kernelVersion() + self.isDebug = False # Packages self.hasEnchant = False # The pyenchant package @@ -484,6 +485,7 @@ def initConfig(self, confPath: str | Path | None = None, self._recentObj.loadCache() self._checkOptionalPackages() + self.isDebug = logger.getEffectiveLevel() == logging.DEBUG logger.debug("Config instance initialised") diff --git a/novelwriter/constants.py b/novelwriter/constants.py index d8ce3562c..4afa77810 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -50,6 +50,9 @@ class nwConst: URL_HELP = "https://github.com/vkbo/novelWriter/discussions" URL_RELEASE = "https://github.com/vkbo/novelWriter/releases/latest" + # Requests + USER_AGENT = "Mozilla/5.0 (compatible; novelWriter (Python))" + # Gui Settings STATUS_MSG_TIMEOUT = 15000 # milliseconds diff --git a/novelwriter/dialogs/updates.py b/novelwriter/dialogs/updates.py index c39a31cca..bbdaf0384 100644 --- a/novelwriter/dialogs/updates.py +++ b/novelwriter/dialogs/updates.py @@ -29,10 +29,10 @@ from datetime import datetime from urllib.request import Request, urlopen -from PyQt5.QtGui import QCursor -from PyQt5.QtCore import Qt +from PyQt5.QtGui import QCloseEvent, QCursor +from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtWidgets import ( - qApp, QDialog, QHBoxLayout, QVBoxLayout, QDialogButtonBox, QLabel + QWidget, qApp, QDialog, QHBoxLayout, QVBoxLayout, QDialogButtonBox, QLabel ) from novelwriter import CONFIG, SHARED, __version__, __date__ @@ -44,8 +44,8 @@ class GuiUpdates(QDialog): - def __init__(self, mainGui): - super().__init__(parent=mainGui) + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) logger.debug("Create: GuiUpdates") self.setObjectName("GuiUpdates") @@ -94,8 +94,8 @@ def __init__(self, mainGui): self.latestLabel.setFont(hFont) # Buttons - self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok) - self.buttonBox.accepted.connect(self._doClose) + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) + self.buttonBox.rejected.connect(self._doClose) # Assemble self.innerBox = QHBoxLayout() @@ -114,17 +114,16 @@ def __init__(self, mainGui): return - def __del__(self): # pragma: no cover + def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiUpdates") return - def checkLatest(self): - """Check for latest release. - """ + def checkLatest(self) -> None: + """Check for latest release.""" qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) urlReq = Request("https://api.github.com/repos/vkbo/novelwriter/releases/latest") - urlReq.add_header("User-Agent", "Mozilla/5.0 (compatible; novelWriter (Python))") + urlReq.add_header("User-Agent", nwConst.USER_AGENT) urlReq.add_header("Accept", "application/vnd.github.v3+json") rawData = {} @@ -161,10 +160,22 @@ def checkLatest(self): return ## - # Internal Functions + # Events ## - def _doClose(self): + def closeEvent(self, event: QCloseEvent) -> None: + """Capture the user closing the window.""" + event.accept() + self.deleteLater() + return + + ## + # Private Slots + ## + + @pyqtSlot() + def _doClose(self) -> None: + """Close the dialog.""" self.close() return diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 8992505db..a0e2c3b90 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -620,7 +620,7 @@ def _dumpHistory(self) -> None: """Debug function to dump history to the logger. Since it is a for loop, it is skipped entirely if log level isn't DEBUG. """ - if logger.getEffectiveLevel() == logging.DEBUG: # pragma: no cover + if CONFIG.isDebug: # pragma: no cover for i, (h, p) in enumerate(zip(self._navHistory, self._posHistory)): logger.debug( "History %02d: %s %13s [x:%d]" % ( diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index a0420144a..8c74f2705 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -870,6 +870,11 @@ def _buildToolsMenu(self) -> None: self.aEditWordList = self.toolsMenu.addAction(self.tr("Project Word List")) self.aEditWordList.triggered.connect(lambda: self.mainGui.showProjectWordListDialog()) + # Tools > Add Dictionaries + if CONFIG.osWindows or CONFIG.isDebug: + self.aAddDicts = self.toolsMenu.addAction(self.tr("Add Dictionaries")) + self.aAddDicts.triggered.connect(self.mainGui.showDictionariesDialog) + # Tools > Separator self.toolsMenu.addSeparator() diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 8e02e0e6b..635e9b8f0 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -59,6 +59,7 @@ from novelwriter.tools.lipsum import GuiLipsum from novelwriter.tools.manuscript import GuiManuscript from novelwriter.tools.projwizard import GuiProjectWizard +from novelwriter.tools.dictionaries import GuiDictionaries from novelwriter.tools.writingstats import GuiWritingStats from novelwriter.core.coretools import ProjectBuilder @@ -340,7 +341,7 @@ def __init__(self) -> None: logger.debug("Ready: GUI") - if __hexversion__[-2] == "a" and logger.getEffectiveLevel() > logging.DEBUG: + if __hexversion__[-2] == "a" and not CONFIG.isDebug: SHARED.warn(self.tr( "You are running an untested development version of novelWriter. " "Please be careful when working on a live project " @@ -1065,6 +1066,20 @@ def showUpdatesDialog(self) -> None: return + @pyqtSlot() + def showDictionariesDialog(self) -> None: + """Show the download dictionaries dialog.""" + dlgDicts = GuiDictionaries(self) + dlgDicts.setModal(True) + dlgDicts.show() + dlgDicts.raise_() + qApp.processEvents() + if not dlgDicts.initDialog(): + dlgDicts.close() + SHARED.error(self.tr("Could not initialise the dialog.")) + + return + def reportConfErr(self) -> bool: """Checks if the Config module has any errors to report, and let the user know if this is the case. The Config module caches diff --git a/novelwriter/tools/dictionaries.py b/novelwriter/tools/dictionaries.py new file mode 100644 index 000000000..847925a02 --- /dev/null +++ b/novelwriter/tools/dictionaries.py @@ -0,0 +1,275 @@ +""" +novelWriter – GUI Dictionary Downloader +======================================= + +File History: +Created: 2023-11-19 [2.2rc1] + +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 . +""" +from __future__ import annotations + +import logging + +from pathlib import Path +from zipfile import ZipFile +from urllib.parse import urljoin +from urllib.request import pathname2url + +from PyQt5.QtGui import QCloseEvent, QDesktopServices, QTextCursor +from PyQt5.QtCore import QUrl, pyqtSlot +from PyQt5.QtWidgets import ( + QDialog, QDialogButtonBox, QFileDialog, QFrame, QHBoxLayout, QLabel, + QLineEdit, QPlainTextEdit, QPushButton, QVBoxLayout, QWidget, qApp +) + +from novelwriter import CONFIG, SHARED +from novelwriter.common import formatInt, getFileSize +from novelwriter.error import formatException + +logger = logging.getLogger(__name__) + + +class GuiDictionaries(QDialog): + + def __init__(self, parent: QWidget) -> None: + super().__init__(parent=parent) + + logger.debug("Create: GuiDictionaries") + self.setObjectName("GuiDictionaries") + self.setWindowTitle(self.tr("Add Dictionaries")) + + self._installPath = None + self._currDicts = set() + + iPx = CONFIG.pxInt(4) + mPx = CONFIG.pxInt(8) + sPx = CONFIG.pxInt(16) + + self.setMinimumWidth(CONFIG.pxInt(500)) + self.setMinimumHeight(CONFIG.pxInt(300)) + + # Hunspell Dictionaries + foUrl = "https://www.freeoffice.com/en/download/dictionaries" + loUrl = "https://extensions.libreoffice.org" + self.huInfo = QLabel("
".join([ + self.tr("Download a dictionary from one of the links, and add it below."), + f" \u203a {foUrl}", + f" \u203a {loUrl}", + ])) + self.huInfo.setOpenExternalLinks(True) + self.huInfo.setWordWrap(True) + self.huInput = QLineEdit(self) + self.huBrowse = QPushButton(self) + self.huBrowse.setIcon(SHARED.theme.getIcon("browse")) + self.huBrowse.clicked.connect(self._doBrowseHunspell) + self.huImport = QPushButton(self.tr("Add Dictionary"), self) + self.huImport.setIcon(SHARED.theme.getIcon("add")) + self.huImport.clicked.connect(self._doImportHunspell) + + self.huPathBox = QHBoxLayout() + self.huPathBox.addWidget(self.huInput) + self.huPathBox.addWidget(self.huBrowse) + self.huPathBox.setSpacing(iPx) + self.huAddBox = QHBoxLayout() + self.huAddBox.addStretch(1) + self.huAddBox.addWidget(self.huImport) + + # Install Path + self.inInfo = QLabel(self.tr("Dictionary install location")) + self.inPath = QLineEdit(self) + self.inPath.setReadOnly(True) + self.inBrowse = QPushButton(self) + self.inBrowse.setIcon(SHARED.theme.getIcon("browse")) + self.inBrowse.clicked.connect(self._doOpenInstallLocation) + + self.inBox = QHBoxLayout() + self.inBox.addWidget(self.inPath) + self.inBox.addWidget(self.inBrowse) + self.inBox.setSpacing(iPx) + + # Info Box + self.infoBox = QPlainTextEdit(self) + self.infoBox.setReadOnly(True) + self.infoBox.setFixedHeight(4*SHARED.theme.fontPixelSize) + self.infoBox.setFrameStyle(QFrame.Shape.NoFrame) + + # Buttons + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) + self.buttonBox.rejected.connect(self._doClose) + + # Assemble + self.innerBox = QVBoxLayout() + self.innerBox.addWidget(self.huInfo) + self.innerBox.addLayout(self.huPathBox) + self.innerBox.addLayout(self.huAddBox) + self.innerBox.addSpacing(mPx) + self.innerBox.addWidget(self.inInfo) + self.innerBox.addLayout(self.inBox) + self.innerBox.addWidget(self.infoBox) + self.innerBox.setSpacing(iPx) + + self.outerBox = QVBoxLayout() + self.outerBox.addLayout(self.innerBox, 0) + self.outerBox.addStretch(1) + self.outerBox.addWidget(self.buttonBox, 0) + self.outerBox.setSpacing(sPx) + + self.setLayout(self.outerBox) + + logger.debug("Ready: GuiDictionaries") + + return + + def __del__(self) -> None: # pragma: no cover + logger.debug("Delete: GuiDictionaries") + return + + def initDialog(self) -> bool: + """Prepare and check that we can proceed.""" + try: + import enchant + path = Path(enchant.get_user_config_dir()) + except Exception: + logger.error("Could not get enchant path") + return False + + self._installPath = Path(path).resolve() + if path.is_dir(): + self.inPath.setText(str(path)) + hunspell = path / "hunspell" + if hunspell.is_dir(): + self._currDicts = set( + i.stem for i in hunspell.iterdir() if i.is_file() and i.suffix == ".aff" + ) + self._appendLog(self.tr( + "Additional dictionaries found: {0}" + ).format(len(self._currDicts))) + + qApp.processEvents() + self.adjustSize() + + return True + + ## + # Events + ## + + def closeEvent(self, event: QCloseEvent) -> None: + """Capture the user closing the window.""" + event.accept() + self.deleteLater() + return + + ## + # Private Slots + ## + + @pyqtSlot() + def _doBrowseHunspell(self): + """Browse for a Free/Libre Office dictionary.""" + extFilter = [ + self.tr("Free or Libre Office extension ({0})").format("*.sox *.oxt"), + self.tr("All files ({0})").format("*"), + ] + soxFile, _ = QFileDialog.getOpenFileName( + self, self.tr("Browse Files"), "", filter=";;".join(extFilter) + ) + if soxFile: + path = Path(soxFile).absolute() + self.huInput.setText(str(path)) + return + + @pyqtSlot() + def _doImportHunspell(self): + """Import a hunspell dictionary from .sox or .oxt file.""" + 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() + def _doOpenInstallLocation(self) -> None: + """Open the dictionary folder.""" + path = self.inPath.text() + if Path(path).is_dir(): + QDesktopServices.openUrl( + QUrl(urljoin("file:", pathname2url(path))) + ) + else: + SHARED.error("Path not found.") + return + + @pyqtSlot() + def _doClose(self) -> None: + """Close the dialog.""" + self.close() + return + + ## + # Internal Functions + ## + + 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.""" + 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/novelwriter/tools/manusbuild.py b/novelwriter/tools/manusbuild.py index 390d626cd..70235ebf1 100644 --- a/novelwriter/tools/manusbuild.py +++ b/novelwriter/tools/manusbuild.py @@ -27,6 +27,7 @@ from pathlib import Path +from PyQt5.QtGui import QCloseEvent from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot from PyQt5.QtWidgets import ( QAbstractButton, QAbstractItemView, QDialog, QDialogButtonBox, QFileDialog, @@ -227,7 +228,7 @@ def __init__(self, parent: QWidget, build: BuildSettings): return - def __del__(self): # pragma: no cover + def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiManuscriptBuild") return @@ -235,7 +236,7 @@ def __del__(self): # pragma: no cover # Events ## - def closeEvent(self, event): + def closeEvent(self, event: QCloseEvent) -> None: """Capture the user closing the window so we can save GUI settings. """ diff --git a/tests/conftest.py b/tests/conftest.py index 1969567e6..1886bfd40 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -159,6 +159,7 @@ def nwGUI(qtbot, monkeypatch, functionFixture): nwGUI = main(["--testmode", f"--config={_TMP_CONF}", f"--data={_TMP_CONF}"]) qtbot.addWidget(nwGUI) resetConfigVars() + SHARED._alert = None nwGUI.docEditor.initEditor() nwGUI.show() diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index e66e5dc61..c00fe9da5 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -35,8 +35,7 @@ @pytest.mark.gui def testGuiMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum): - """Test the main menu Edit and Format entries. - """ + """Test the main menu Edit and Format entries.""" monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True) # Test Document Action with No Project @@ -343,8 +342,7 @@ def testGuiMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum): @pytest.mark.gui def testGuiMenu_ContextMenus(qtbot, nwGUI, prjLipsum): - """Test the context menus. - """ + """Test the context menus.""" assert nwGUI.openProject(prjLipsum) assert nwGUI.openDocument("4c4f28287af27") @@ -423,8 +421,7 @@ def testGuiMenu_ContextMenus(qtbot, nwGUI, prjLipsum): @pytest.mark.gui def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): - """Test the Insert menu. - """ + """Test the Insert menu.""" buildTestProject(nwGUI, projPath) assert nwGUI.projView.projTree._getTreeItem(C.hSceneDoc) is not None diff --git a/tests/test_tools/test_tools_dictionaries.py b/tests/test_tools/test_tools_dictionaries.py new file mode 100644 index 000000000..5c3b471e0 --- /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