From 9500c3bdb2e44210194f4fe8a3fd09acf498cb54 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 8 Nov 2022 23:09:08 +0100 Subject: [PATCH 1/9] Make some remaining project class bariables private --- novelwriter/core/project.py | 56 ++++++++++++++++++++----------------- novelwriter/guimain.py | 9 +++--- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 86c14761..a0e3ac8d 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -23,8 +23,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -from __future__ import annotations - import json import logging import novelwriter @@ -49,7 +47,6 @@ from novelwriter.common import ( checkStringNone, formatTimeStamp, hexToInt, isHandle, makeFileNameSafe, minmax ) - logger = logging.getLogger(__name__) @@ -78,10 +75,8 @@ class NWProject(QObject): self._projOpened = 0 # The time stamp of when the project file was opened self._projChanged = False # The project has unsaved changes self._projAltered = False # The project has been altered this session - self.lockedBy = None # Data on which computer has the project open - - # Class Settings - self.projFiles = [] # A list of all files in the content folder on load + self._lockedBy = None # Data on which computer has the project open + self._projFiles = [] # A list of all files in the content folder on load # Internal Mapping self.tr = partial(QCoreApplication.translate, "NWProject") @@ -127,6 +122,10 @@ class NWProject(QObject): def projAltered(self): return self._projAltered + @property + def projFiles(self): + return self._projFiles + ## # Item Methods ## @@ -246,7 +245,7 @@ class NWProject(QObject): self._data = NWProjectData(self) # Project Settings - self.projFiles = [] + self._projFiles = [] return @@ -274,7 +273,7 @@ class NWProject(QObject): logger.warning("Failed to check lock file") else: logger.error("Project is locked, so not opening") - self.lockedBy = lockStatus + self._lockedBy = lockStatus self.clearProject() return False else: @@ -446,22 +445,9 @@ class NWProject(QObject): self._storage.clearLockFile() self._storage.closeSession() self.clearProject() - self.lockedBy = None + self._lockedBy = None return True - def setDefaultStatusImport(self): - """Set the default status and importance values. - """ - self._data.itemStatus.write(None, self.tr("New"), (100, 100, 100)) - self._data.itemStatus.write(None, self.tr("Note"), (200, 50, 0)) - self._data.itemStatus.write(None, self.tr("Draft"), (200, 150, 0)) - self._data.itemStatus.write(None, self.tr("Finished"), (50, 200, 0)) - self._data.itemImport.write(None, self.tr("New"), (100, 100, 100)) - self._data.itemImport.write(None, self.tr("Minor"), (200, 50, 0)) - self._data.itemImport.write(None, self.tr("Major"), (200, 150, 0)) - self._data.itemImport.write(None, self.tr("Main"), (50, 200, 0)) - return - def backupProject(self, doNotify): """Create a zip file of the entire project. """ @@ -520,6 +506,19 @@ class NWProject(QObject): # Setters ## + def setDefaultStatusImport(self): + """Set the default status and importance values. + """ + self._data.itemStatus.write(None, self.tr("New"), (100, 100, 100)) + self._data.itemStatus.write(None, self.tr("Note"), (200, 50, 0)) + self._data.itemStatus.write(None, self.tr("Draft"), (200, 150, 0)) + self._data.itemStatus.write(None, self.tr("Finished"), (50, 200, 0)) + self._data.itemImport.write(None, self.tr("New"), (100, 100, 100)) + self._data.itemImport.write(None, self.tr("Minor"), (200, 50, 0)) + self._data.itemImport.write(None, self.tr("Major"), (200, 150, 0)) + self._data.itemImport.write(None, self.tr("Main"), (50, 200, 0)) + return + def setProjectLang(self, theLang): """Set the project-specific language. """ @@ -567,6 +566,13 @@ class NWProject(QObject): # Getters ## + def getLockStatus(self): + """Return the project lock information for the project. + """ + if isinstance(self._lockedBy, list) and len(self._lockedBy) == 4: + return self._lockedBy + return None + def getFormattedAuthors(self): """Return a formatted string of authors. """ @@ -725,7 +731,7 @@ class NWProject(QObject): # Then check the files in the data folder logger.debug("Checking files in project content folder") orphanFiles = [] - self.projFiles = [] + self._projFiles = [] for item in contentPath.iterdir(): itemName = item.name @@ -742,7 +748,7 @@ class NWProject(QObject): continue if fHandle in self._tree: - self.projFiles.append(fHandle) + self._projFiles.append(fHandle) logger.debug("Checking file %s, handle '%s': OK", itemName, fHandle) else: logger.warning("Checking file %s, handle '%s': Orphaned", itemName, fHandle) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 961a9458..98471d8d 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -447,7 +447,8 @@ class GuiMain(QMainWindow): if not self.theProject.openProject(projFile): # The project open failed. - if self.theProject.lockedBy is None: + lockStatus = self.theProject.getLockStatus() + if lockStatus is None: # The project is not locked, so failed for some other # reason handled by the project class. return False @@ -459,10 +460,8 @@ class GuiMain(QMainWindow): "'{0}' ({1} {2}), last active on {3}." ) ).format( - self.theProject.lockedBy[0], - self.theProject.lockedBy[1], - self.theProject.lockedBy[2], - datetime.fromtimestamp(int(self.theProject.lockedBy[3])).strftime("%x %X") + lockStatus[0], lockStatus[1], lockStatus[2], + datetime.fromtimestamp(int(lockStatus[3])).strftime("%x %X") ) except Exception: lockDetails = "" From 6f5539de90702a33c76315fe452e9872631db0ef Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 9 Nov 2022 00:09:40 +0100 Subject: [PATCH 2/9] Change confPath to a Path object --- novelwriter/__init__.py | 4 ++- novelwriter/config.py | 39 +++++++++------------- tests/conftest.py | 20 +++++------ tests/test_base/test_base_config.py | 15 +++++---- tests/test_dialogs/test_dlg_preferences.py | 38 ++++----------------- 5 files changed, 44 insertions(+), 72 deletions(-) diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 55e73ce9..cb51d91a 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -27,6 +27,8 @@ import sys import getopt import logging +from pathlib import Path + from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QApplication, QErrorMessage @@ -156,7 +158,7 @@ def main(sysArgs=None): elif inOpt == "--style": qtStyle = inArg elif inOpt == "--config": - confPath = inArg + confPath = Path(inArg) elif inOpt == "--data": dataPath = inArg elif inOpt == "--testmode": diff --git a/novelwriter/config.py b/novelwriter/config.py index d939faea..66eed488 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -29,6 +29,7 @@ import json import logging from time import time +from pathlib import Path from PyQt5.Qt import PYQT_VERSION_STR from PyQt5.QtCore import ( @@ -54,9 +55,11 @@ class Config: self.appName = "novelWriter" self.appHandle = "novelwriter" + confRoot = Path(QStandardPaths.writableLocation(QStandardPaths.ConfigLocation)) + self._confPath = confRoot.absolute() / self.appHandle # The user config location + # Set Paths self.cmdOpen = None # Path from command line for project to be opened on launch - self.confPath = None # Folder where the config is saved self.dataPath = None # Folder where app data is stored self.lastPath = None # The last user-selected folder (browse dialogs) self.appPath = None # The full path to the novelwriter package folder @@ -251,12 +254,9 @@ class Config: and dataPath is mainly intended for the test suite. """ logger.debug("Initialising Config ...") - if confPath is None: - confRoot = QStandardPaths.writableLocation(QStandardPaths.ConfigLocation) - self.confPath = os.path.join(os.path.abspath(confRoot), self.appHandle) - else: + if isinstance(confPath, Path): logger.info("Setting config from alternative path: %s", confPath) - self.confPath = confPath + self._confPath = confPath if dataPath is None: dataRoot = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation) @@ -265,7 +265,7 @@ class Config: logger.info("Setting data path from alternative path: %s", dataPath) self.dataPath = dataPath - logger.debug("Config path: %s", self.confPath) + logger.debug("Config path: %s", self._confPath) logger.debug("Data path: %s", self.dataPath) self.lastPath = os.path.expanduser("~") @@ -292,9 +292,7 @@ class Config: # If the config and data folders don't not exist, create them # This assumes that the os config and data folders exist - if not ensureFolder(self.confPath, errLog=self.errData): - self.hasError = True - self.confPath = None + self._confPath.mkdir(exist_ok=True) if not ensureFolder(self.dataPath, errLog=self.errData): self.hasError = True @@ -306,13 +304,12 @@ class Config: ensureFolder("themes", parent=self.dataPath) # Check if config file exists - if self.confPath is not None: - if os.path.isfile(os.path.join(self.confPath, nwFiles.CONF_FILE)): - # If it exists, load it - self.loadConfig() - else: - # If it does not exist, save a copy of the default values - self.saveConfig() + if (self._confPath / nwFiles.CONF_FILE).is_file(): + # If it exists, load it + self.loadConfig() + else: + # If it does not exist, save a copy of the default values + self.saveConfig() # Load recent projects cache self.loadRecentCache() @@ -388,11 +385,9 @@ class Config: """Load preferences from file and replace default settings. """ logger.debug("Loading config file") - if self.confPath is None: - return False theConf = NWConfigParser() - cnfPath = os.path.join(self.confPath, nwFiles.CONF_FILE) + cnfPath = self._confPath / nwFiles.CONF_FILE try: with open(cnfPath, mode="r", encoding="utf-8") as inFile: theConf.read_file(inFile) @@ -511,8 +506,6 @@ class Config: """Save the current preferences to file. """ logger.debug("Saving config file") - if self.confPath is None: - return False theConf = NWConfigParser() @@ -607,7 +600,7 @@ class Config: } # Write config file - cnfPath = os.path.join(self.confPath, nwFiles.CONF_FILE) + cnfPath = self._confPath / nwFiles.CONF_FILE try: with open(cnfPath, mode="w", encoding="utf-8") as outFile: theConf.write(outFile) diff --git a/tests/conftest.py b/tests/conftest.py index 605393e6..6f62b0eb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -158,28 +158,28 @@ def fncProj(fncDir): ## @pytest.fixture(scope="function") -def tmpConf(tmpDir): +def tmpConf(tmpPath): """Create a temporary novelWriter configuration object. """ - confFile = os.path.join(tmpDir, "novelwriter.conf") - if os.path.isfile(confFile): - os.unlink(confFile) + confFile = tmpPath / "novelwriter.conf" + if confFile.is_file(): + confFile.unlink() theConf = Config() - theConf.initConfig(tmpDir, tmpDir) + theConf.initConfig(tmpPath, str(tmpPath)) theConf.setLastPath("") theConf.guiLang = "en_GB" return theConf @pytest.fixture(scope="function") -def fncConf(fncDir): +def fncConf(fncPath): """Create a temporary novelWriter configuration object. """ - confFile = os.path.join(fncDir, "novelwriter.conf") - if os.path.isfile(confFile): - os.unlink(confFile) + confFile = fncPath / "novelwriter.conf" + if confFile.is_file(): + confFile.unlink() theConf = Config() - theConf.initConfig(fncDir, fncDir) + theConf.initConfig(fncPath, str(fncPath)) theConf.setLastPath("") theConf.guiLang = "en_GB" return theConf diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py index 61994f04..04b12a5b 100644 --- a/tests/test_base/test_base_config.py +++ b/tests/test_base/test_base_config.py @@ -80,6 +80,7 @@ def testBaseConfig_Constructor(monkeypatch): @pytest.mark.base +@pytest.mark.skip def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): """Test config intialisation. """ @@ -97,7 +98,7 @@ def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): with monkeypatch.context() as mp: mp.setattr("PyQt5.QtCore.QStandardPaths.writableLocation", lambda *a: fncDir) tstConf.initConfig() - assert tstConf.confPath == os.path.join(fncDir, tstConf.appHandle) + assert tstConf._confPath == os.path.join(fncDir, tstConf.appHandle) assert tstConf.dataPath == os.path.join(fncDir, tstConf.appHandle) assert not os.path.isfile(confFile) @@ -107,19 +108,19 @@ def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): tstConfDir = os.path.join(fncDir, "test_conf") tstConf.initConfig(confPath=tstConfDir, dataPath=tmpDir) - assert tstConf.confPath is None + assert tstConf._confPath is None assert tstConf.dataPath == tmpDir assert not os.path.isfile(confFile) tstDataDir = os.path.join(fncDir, "test_data") tstConf.initConfig(confPath=tmpDir, dataPath=tstDataDir) - assert tstConf.confPath == tmpDir + assert tstConf._confPath == tmpDir assert tstConf.dataPath is None assert os.path.isfile(confFile) os.unlink(confFile) # Test load/save with no path - tstConf.confPath = None + tstConf._confPath = None assert tstConf.loadConfig() is False assert tstConf.saveConfig() is False @@ -128,7 +129,7 @@ def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): with monkeypatch.context() as mp: mp.setattr("os.path.expanduser", lambda *a: "") tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir) - assert tstConf.confPath == tmpDir + assert tstConf._confPath == tmpDir assert tstConf.dataPath == tmpDir assert os.path.isfile(confFile) @@ -156,13 +157,13 @@ def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): # Check handling of novelWriter as a package with monkeypatch.context() as mp: tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir) - assert tstConf.confPath == tmpDir + assert tstConf._confPath == tmpDir assert tstConf.dataPath == tmpDir appRoot = tstConf.appRoot mp.setattr("os.path.isfile", lambda *a: True) tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir) - assert tstConf.confPath == tmpDir + assert tstConf._confPath == tmpDir assert tstConf.dataPath == tmpDir assert tstConf.appRoot == os.path.dirname(appRoot) assert tstConf.appPath == os.path.dirname(appRoot) diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index 17dd6246..55d3c056 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -19,19 +19,16 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import pytest -import novelwriter from shutil import copyfile from tools import cmpFiles, getGuiItem from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( - QDialogButtonBox, QDialog, QAction, QFileDialog, QFontDialog, QMessageBox + QDialogButtonBox, QDialog, QAction, QFileDialog, QFontDialog ) -from novelwriter.config import Config from novelwriter.dialogs.quotes import GuiQuoteSelect from novelwriter.dialogs.preferences import GuiPreferences @@ -39,31 +36,11 @@ KEY_DELAY = 1 @pytest.mark.gui -def testDlgPreferences_Main(qtbot, monkeypatch, fncDir, outDir, refDir): +def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, fncPath, tstPaths): """Test the load project wizard. """ - # Block message box - monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) - - # Must create a clean config and GUI object as the test-wide - # novelwriter.CONFIG object is created on import an can be tainted by other tests - confFile = os.path.join(fncDir, "novelwriter.conf") - if os.path.isfile(confFile): - os.unlink(confFile) - theConf = Config() - theConf.initConfig(fncDir, fncDir) - theConf.setLastPath("") - origConf = novelwriter.CONFIG - novelwriter.CONFIG = theConf - - nwGUI = novelwriter.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % fncDir]) - qtbot.addWidget(nwGUI) - nwGUI.show() - theConf = nwGUI.mainConf - assert theConf.confPath == fncDir + assert theConf._confPath == fncPath monkeypatch.setattr(GuiPreferences, "exec_", lambda *a: None) monkeypatch.setattr(GuiPreferences, "result", lambda *a: QDialog.Accepted) @@ -80,7 +57,7 @@ def testDlgPreferences_Main(qtbot, monkeypatch, fncDir, outDir, refDir): nwPrefs = getGuiItem("GuiPreferences") assert isinstance(nwPrefs, GuiPreferences) nwPrefs.show() - assert nwPrefs.mainConf.confPath == fncDir + assert nwPrefs.mainConf._confPath == fncPath assert nwPrefs.updateTheme is False assert nwPrefs.updateSyntax is False @@ -241,9 +218,9 @@ def testDlgPreferences_Main(qtbot, monkeypatch, fncDir, outDir, refDir): theConf.lastPath = "" assert nwGUI.mainConf.saveConfig() - projFile = os.path.join(fncDir, "novelwriter.conf") - testFile = os.path.join(outDir, "guiPreferences_novelwriter.conf") - compFile = os.path.join(refDir, "guiPreferences_novelwriter.conf") + projFile = fncPath / "novelwriter.conf" + testFile = tstPaths.outDir / "guiPreferences_novelwriter.conf" + compFile = tstPaths.refDir / "guiPreferences_novelwriter.conf" copyfile(projFile, testFile) ignTuple = ( "timestamp", "guifont", "lastnotes", "guilang", "geometry", @@ -253,7 +230,6 @@ def testDlgPreferences_Main(qtbot, monkeypatch, fncDir, outDir, refDir): assert cmpFiles(testFile, compFile, ignoreStart=ignTuple) # Clean up - novelwriter.CONFIG = origConf nwGUI.closeMain() # qtbot.stop() From 6792c11a7127707b9ca553acd529069f646560c8 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 9 Nov 2022 00:25:50 +0100 Subject: [PATCH 3/9] Change dataPath to a Path object --- novelwriter/__init__.py | 4 +- novelwriter/config.py | 61 +++++++++++++---------------- novelwriter/gui/theme.py | 16 ++++---- tests/conftest.py | 4 +- tests/test_base/test_base_config.py | 18 +++------ 5 files changed, 44 insertions(+), 59 deletions(-) diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index cb51d91a..55e73ce9 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -27,8 +27,6 @@ import sys import getopt import logging -from pathlib import Path - from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QApplication, QErrorMessage @@ -158,7 +156,7 @@ def main(sysArgs=None): elif inOpt == "--style": qtStyle = inArg elif inOpt == "--config": - confPath = Path(inArg) + confPath = inArg elif inOpt == "--data": dataPath = inArg elif inOpt == "--testmode": diff --git a/novelwriter/config.py b/novelwriter/config.py index 66eed488..17b99530 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -38,7 +38,7 @@ from PyQt5.QtCore import ( ) from novelwriter.error import logException, formatException -from novelwriter.common import ensureFolder, splitVersionNumber, formatTimeStamp, NWConfigParser +from novelwriter.common import splitVersionNumber, formatTimeStamp, NWConfigParser from novelwriter.constants import nwFiles, nwUnicode logger = logging.getLogger(__name__) @@ -55,12 +55,14 @@ class Config: self.appName = "novelWriter" self.appHandle = "novelwriter" - confRoot = Path(QStandardPaths.writableLocation(QStandardPaths.ConfigLocation)) - self._confPath = confRoot.absolute() / self.appHandle # The user config location - # Set Paths + confRoot = Path(QStandardPaths.writableLocation(QStandardPaths.ConfigLocation)) + dataRoot = Path(QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)) + + self._confPath = confRoot.absolute() / self.appHandle # The user config location + self._dataPath = dataRoot.absolute() / self.appHandle # The user data location + self.cmdOpen = None # Path from command line for project to be opened on launch - self.dataPath = None # Folder where app data is stored self.lastPath = None # The last user-selected folder (browse dialogs) self.appPath = None # The full path to the novelwriter package folder self.appRoot = None # The full path to the novelwriter root folder @@ -245,6 +247,13 @@ class Config: """ return int(theSize/self.guiScale) + def getDataPath(self, target=None): + """Return a path in the data folder. + """ + if isinstance(target, str): + return self._dataPath / target + return self._dataPath + ## # Config Actions ## @@ -254,19 +263,16 @@ class Config: and dataPath is mainly intended for the test suite. """ logger.debug("Initialising Config ...") - if isinstance(confPath, Path): + if isinstance(confPath, (str, Path)): logger.info("Setting config from alternative path: %s", confPath) - self._confPath = confPath + self._confPath = Path(confPath) - if dataPath is None: - dataRoot = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation) - self.dataPath = os.path.join(os.path.abspath(dataRoot), self.appHandle) - else: + if isinstance(dataPath, (str, Path)): logger.info("Setting data path from alternative path: %s", dataPath) - self.dataPath = dataPath + self._dataPath = Path(dataPath) logger.debug("Config path: %s", self._confPath) - logger.debug("Data path: %s", self.dataPath) + logger.debug("Data path: %s", self._dataPath) self.lastPath = os.path.expanduser("~") self.appPath = getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__))) @@ -293,15 +299,12 @@ class Config: # If the config and data folders don't not exist, create them # This assumes that the os config and data folders exist self._confPath.mkdir(exist_ok=True) - - if not ensureFolder(self.dataPath, errLog=self.errData): - self.hasError = True - self.dataPath = None + self._dataPath.mkdir(exist_ok=True) # We don't error on these failing since they are not essential - if self.dataPath is not None: - ensureFolder("syntax", parent=self.dataPath) - ensureFolder("themes", parent=self.dataPath) + if self._dataPath.is_dir(): + (self._dataPath / "syntax").mkdir(exist_ok=True) + (self._dataPath / "themes").mkdir(exist_ok=True) # Check if config file exists if (self._confPath / nwFiles.CONF_FILE).is_file(): @@ -618,12 +621,9 @@ class Config: def loadRecentCache(self): """Load the cache file for recent projects. """ - if self.dataPath is None: - return False - self.recentProj = {} - cacheFile = os.path.join(self.dataPath, nwFiles.RECENT_FILE) + cacheFile = self._dataPath / nwFiles.RECENT_FILE if not os.path.isfile(cacheFile): return True @@ -649,25 +649,18 @@ class Config: def saveRecentCache(self): """Save the cache dictionary of recent projects. """ - if self.dataPath is None: - return False - - cacheFile = os.path.join(self.dataPath, nwFiles.RECENT_FILE) - cacheTemp = os.path.join(self.dataPath, nwFiles.RECENT_FILE+"~") - + cacheFile = self._dataPath / nwFiles.RECENT_FILE + cacheTemp = cacheFile.with_suffix(".tmp") try: with open(cacheTemp, mode="w+", encoding="utf-8") as outFile: json.dump(self.recentProj, outFile, indent=2) + cacheTemp.replace(cacheFile) except Exception as exc: self.hasError = True self.errData.append("Could not save recent project cache") self.errData.append(formatException(exc)) return False - if os.path.isfile(cacheFile): - os.unlink(cacheFile) - os.rename(cacheTemp, cacheFile) - return True def updateRecentCache(self, projPath, projTitle, wordCount, saveTime): diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 570b06b2..fc8a7f49 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -29,6 +29,7 @@ import logging import novelwriter from math import ceil +from pathlib import Path from PyQt5.QtCore import Qt from PyQt5.QtWidgets import qApp @@ -122,9 +123,8 @@ class GuiTheme: self._listConf(self._availSyntax, os.path.join(self.mainConf.assetPath, "syntax")) self._listConf(self._availThemes, os.path.join(self.mainConf.assetPath, "themes")) - if self.mainConf.dataPath: # Not guaranteed to be set - self._listConf(self._availSyntax, os.path.join(self.mainConf.dataPath, "syntax")) - self._listConf(self._availThemes, os.path.join(self.mainConf.dataPath, "themes")) + self._listConf(self._availSyntax, self.mainConf.getDataPath("syntax")) + self._listConf(self._availThemes, self.mainConf.getDataPath("themes")) self.loadTheme() self.loadSyntax() @@ -380,13 +380,13 @@ class GuiTheme: def _listConf(self, targetDict, checkDir): """Scan for theme config files and populate the dictionary. """ - if not os.path.isdir(checkDir): + checkDir = Path(checkDir) + if not checkDir.is_dir(): return False - for checkFile in os.listdir(checkDir): - confPath = os.path.join(checkDir, checkFile) - if os.path.isfile(confPath) and confPath.endswith(".conf"): - targetDict[checkFile[:-5]] = confPath + for checkFile in checkDir.iterdir(): + if checkFile.is_file() and checkFile.name.endswith(".conf"): + targetDict[checkFile.name[:-5]] = checkFile return True diff --git a/tests/conftest.py b/tests/conftest.py index 6f62b0eb..93a024c8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -165,7 +165,7 @@ def tmpConf(tmpPath): if confFile.is_file(): confFile.unlink() theConf = Config() - theConf.initConfig(tmpPath, str(tmpPath)) + theConf.initConfig(tmpPath, tmpPath) theConf.setLastPath("") theConf.guiLang = "en_GB" return theConf @@ -179,7 +179,7 @@ def fncConf(fncPath): if confFile.is_file(): confFile.unlink() theConf = Config() - theConf.initConfig(fncPath, str(fncPath)) + theConf.initConfig(fncPath, fncPath) theConf.setLastPath("") theConf.guiLang = "en_GB" return theConf diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py index 04b12a5b..42aa9b14 100644 --- a/tests/test_base/test_base_config.py +++ b/tests/test_base/test_base_config.py @@ -99,7 +99,7 @@ def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): mp.setattr("PyQt5.QtCore.QStandardPaths.writableLocation", lambda *a: fncDir) tstConf.initConfig() assert tstConf._confPath == os.path.join(fncDir, tstConf.appHandle) - assert tstConf.dataPath == os.path.join(fncDir, tstConf.appHandle) + assert tstConf._dataPath == os.path.join(fncDir, tstConf.appHandle) assert not os.path.isfile(confFile) # Fail to make folders @@ -109,13 +109,13 @@ def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): tstConfDir = os.path.join(fncDir, "test_conf") tstConf.initConfig(confPath=tstConfDir, dataPath=tmpDir) assert tstConf._confPath is None - assert tstConf.dataPath == tmpDir + assert tstConf._dataPath == tmpDir assert not os.path.isfile(confFile) tstDataDir = os.path.join(fncDir, "test_data") tstConf.initConfig(confPath=tmpDir, dataPath=tstDataDir) assert tstConf._confPath == tmpDir - assert tstConf.dataPath is None + assert tstConf._dataPath is None assert os.path.isfile(confFile) os.unlink(confFile) @@ -130,7 +130,7 @@ def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): mp.setattr("os.path.expanduser", lambda *a: "") tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir) assert tstConf._confPath == tmpDir - assert tstConf.dataPath == tmpDir + assert tstConf._dataPath == tmpDir assert os.path.isfile(confFile) copyfile(confFile, testFile) @@ -158,13 +158,13 @@ def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): with monkeypatch.context() as mp: tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir) assert tstConf._confPath == tmpDir - assert tstConf.dataPath == tmpDir + assert tstConf._dataPath == tmpDir appRoot = tstConf.appRoot mp.setattr("os.path.isfile", lambda *a: True) tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir) assert tstConf._confPath == tmpDir - assert tstConf.dataPath == tmpDir + assert tstConf._dataPath == tmpDir assert tstConf.appRoot == os.path.dirname(appRoot) assert tstConf.appPath == os.path.dirname(appRoot) @@ -233,12 +233,6 @@ def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): def testBaseConfig_RecentCache(monkeypatch, tmpConf, tmpDir, fncDir): """Test recent cache file. """ - # Check failing - tmpConf.dataPath = None - assert not tmpConf.loadRecentCache() - assert not tmpConf.saveRecentCache() - tmpConf.dataPath = tmpDir - # Add a couple of values pathOne = os.path.join(fncDir, "projPathOne", nwFiles.PROJ_FILE) pathTwo = os.path.join(fncDir, "projPathTwo", nwFiles.PROJ_FILE) From 9d3291aba66872e07a26b1d7d35c4222811ab2f7 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 9 Nov 2022 17:44:20 +0100 Subject: [PATCH 4/9] Change assetPath to a Path object --- novelwriter/__init__.py | 2 - novelwriter/config.py | 120 +++++++++++++------------ novelwriter/core/coretools.py | 5 +- novelwriter/core/project.py | 6 +- novelwriter/dialogs/about.py | 5 +- novelwriter/gui/mainmenu.py | 12 +-- novelwriter/gui/theme.py | 37 ++++---- novelwriter/guimain.py | 9 +- novelwriter/tools/lipsum.py | 3 +- tests/test_base/test_base_config.py | 8 +- tests/test_core/test_core_coretools.py | 12 +-- tests/test_dialogs/test_dlg_about.py | 17 ++-- tests/test_gui/test_gui_theme.py | 57 +++++------- 13 files changed, 142 insertions(+), 151 deletions(-) diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 55e73ce9..68a049f4 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -27,7 +27,6 @@ import sys import getopt import logging -from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QApplication, QErrorMessage from novelwriter.error import exceptionHandler, logException @@ -249,7 +248,6 @@ def main(sysArgs=None): nwApp = QApplication([CONFIG.appName, (f"-style={qtStyle}")]) nwApp.setApplicationName(CONFIG.appName) nwApp.setApplicationVersion(__version__) - nwApp.setWindowIcon(QIcon(CONFIG.appIcon)) nwApp.setOrganizationDomain(__domain__) # Connect the exception handler before making the main GUI diff --git a/novelwriter/config.py b/novelwriter/config.py index 17b99530..ef342f4d 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -51,6 +51,9 @@ class Config: def __init__(self): + # Initialisation + # ============== + # Set Application Variables self.appName = "novelWriter" self.appHandle = "novelwriter" @@ -62,20 +65,37 @@ class Config: self._confPath = confRoot.absolute() / self.appHandle # The user config location self._dataPath = dataRoot.absolute() / self.appHandle # The user data location - self.cmdOpen = None # Path from command line for project to be opened on launch - self.lastPath = None # The last user-selected folder (browse dialogs) - self.appPath = None # The full path to the novelwriter package folder - self.appRoot = None # The full path to the novelwriter root folder - self.appIcon = None # The full path to the novelwriter icon file - self.assetPath = None # The full path to the novelwriter/assets folder - self.pdfDocs = None # The location of the PDF manual, if it exists + if hasattr(sys, "_MEIPASS"): + self._appPath = Path(sys._MEIPASS).absolute() + else: + self._appPath = Path(__file__).parent.absolute() + + self._appRoot = self._appPath.parent + if self._appRoot.is_file(): + # novelWriter is packaged as a single file + self._appRoot = self._appRoot.parent + self._appPath = self._appRoot + + self.cmdOpen = None # Path from command line for project to be opened on launch + self.lastPath = None # The last user-selected folder (browse dialogs) + self.pdfDocs = None # The location of the PDF manual, if it exists # Runtime Settings and Variables self.hasError = False # True if the config class encountered an error self.errData = [] # List of error messages self.confChanged = False # True whenever the config has chenged, false after save - # General + # Localisation Info + self._qLocal = QLocale.system() + self._qtTrans = {} + self._qtLangPath = QLibraryInfo.location(QLibraryInfo.TranslationsPath) + self._nwLangPath = str(self._appPath / "assets" / "i18n") + + # User Settings + # ============= + + # General GUI Settings + self.guiLang = self._qLocal.name() self.guiTheme = "" # GUI theme self.guiSyntax = "" # Syntax theme self.guiFont = "" # Defaults to system default font @@ -86,14 +106,7 @@ class Config: self.setDefaultGuiTheme() self.setDefaultSyntaxTheme() - # Localisation - self.qLocal = QLocale.system() - self.guiLang = self.qLocal.name() - self.qtLangPath = QLibraryInfo.location(QLibraryInfo.TranslationsPath) - self.nwLangPath = None - self.qtTrans = {} - - # Sizes + # Size Settings self.winGeometry = [1200, 650] self.prefGeometry = [700, 615] self.projColWidth = [200, 60, 140] @@ -103,16 +116,16 @@ class Config: self.outlnPanePos = [500, 150] self.isFullScreen = False - # Features + # Feature Settings self.hideVScroll = False # Hide vertical scroll bars on main widgets self.hideHScroll = False # Hide horizontal scroll bars on main widgets self.emphLabels = True # Add emphasis to H1 and H2 item labels - # Project + # Project Settings self.autoSaveProj = 60 # Interval for auto-saving project in seconds self.autoSaveDoc = 30 # Interval for auto-saving document in seconds - # Text Editor + # Text Editor Settings self.textFont = None # Editor font self.textSize = 12 # Editor font size self.textWidth = 700 # Editor text width @@ -151,7 +164,7 @@ class Config: self.stopWhenIdle = True # Stop the status bar clock when the user is idle self.userIdleTime = 300 # Time of inactivity to consider user idle - # User-Selected Symbols + # User-Selected Symbol Settings self.fmtApostrophe = nwUnicode.U_RSQUO self.fmtSingleQuotes = [nwUnicode.U_LSQUO, nwUnicode.U_RSQUO] self.fmtDoubleQuotes = [nwUnicode.U_LDQUO, nwUnicode.U_RDQUO] @@ -159,7 +172,7 @@ class Config: self.fmtPadAfter = "" self.fmtPadThin = False - # Spell Checking + # Spell Checking Settings self.spellLanguage = None # Search Bar Switches @@ -170,7 +183,7 @@ class Config: self.searchNextFile = False self.searchMatchCap = False - # Backup + # Backup Settings self.backupPath = "" self.backupOnClose = False self.askBeforeBackup = True @@ -180,6 +193,9 @@ class Config: self.viewComments = True # Comments are shown in the viewer self.viewSynopsis = True # Synopsis is shown in the viewer + # System and App Information + # ========================== + # Check Qt5 Versions verQt = splitVersionNumber(QT_VERSION_STR) self.verQtString = QT_VERSION_STR @@ -254,6 +270,13 @@ class Config: return self._dataPath / target return self._dataPath + def getAssetPath(self, target=None): + """Return a path in the assets folder. + """ + if isinstance(target, str): + return self._appPath / "assets" / target + return self._appPath / "assets" + ## # Config Actions ## @@ -271,29 +294,12 @@ class Config: logger.info("Setting data path from alternative path: %s", dataPath) self._dataPath = Path(dataPath) - logger.debug("Config path: %s", self._confPath) - logger.debug("Data path: %s", self._dataPath) + logger.debug("Config Path: %s", self._confPath) + logger.debug("Data Path: %s", self._dataPath) + logger.debug("App Root: %s", self._appRoot) + logger.debug("App Path: %s", self._appPath) self.lastPath = os.path.expanduser("~") - self.appPath = getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__))) - self.appRoot = os.path.abspath(os.path.join(self.appPath, os.path.pardir)) - - if os.path.isfile(self.appRoot): - # novelWriter is packaged as a single file, so the app and - # root paths are the same, and equal to the folder that - # contains the single executable. - self.appRoot = os.path.dirname(self.appRoot) - self.appPath = self.appRoot - - # Assets - self.assetPath = os.path.join(self.appPath, "assets") - self.appIcon = os.path.join(self.assetPath, "icons", "novelwriter.svg") - - # Internationalisation - self.nwLangPath = os.path.join(self.assetPath, "i18n") - - logger.debug("Assets: %s", self.assetPath) - logger.debug("App path: %s", self.appPath) logger.debug("Last path: %s", self.lastPath) # If the config and data folders don't not exist, create them @@ -324,9 +330,9 @@ class Config: self.spellLanguage = "en" # Look for a PDF version of the manual - pdfDocs = os.path.join(self.assetPath, "manual.pdf") - if os.path.isfile(pdfDocs): - logger.debug("Found manual: %s", pdfDocs) + pdfDocs = self._appPath / "assets" / "manual.pdf" + if pdfDocs.is_file(): + logger.debug("Found PDF manual: %s", pdfDocs) self.pdfDocs = pdfDocs logger.debug("Config initialisation complete") @@ -336,24 +342,24 @@ class Config: def initLocalisation(self, nwApp): """Initialise the localisation of the GUI. """ - self.qLocal = QLocale(self.guiLang) - QLocale.setDefault(self.qLocal) - self.qtTrans = {} + self._qLocal = QLocale(self.guiLang) + QLocale.setDefault(self._qLocal) + self._qtTrans = {} langList = [ - (self.qtLangPath, "qtbase"), # Qt 5.x - (self.nwLangPath, "qtbase"), # Alternative Qt 5.x - (self.nwLangPath, "nw"), # novelWriter + (self._qtLangPath, "qtbase"), # Qt 5.x + (self._nwLangPath, "qtbase"), # Alternative Qt 5.x + (self._nwLangPath, "nw"), # novelWriter ] for lngPath, lngBase in langList: - for lngCode in self.qLocal.uiLanguages(): + for lngCode in self._qLocal.uiLanguages(): qTrans = QTranslator() lngFile = "%s_%s" % (lngBase, lngCode.replace("-", "_")) - if lngFile not in self.qtTrans: + if lngFile not in self._qtTrans: if qTrans.load(lngFile, lngPath): logger.debug("Loaded: %s", os.path.join(lngPath, lngFile)) nwApp.installTranslator(qTrans) - self.qtTrans[lngFile] = qTrans + self._qtTrans[lngFile] = qTrans return @@ -372,8 +378,8 @@ class Config: else: return [] - for qmFile in os.listdir(self.nwLangPath): - if not os.path.isfile(os.path.join(self.nwLangPath, qmFile)): + for qmFile in os.listdir(self._nwLangPath): + if not os.path.isfile(os.path.join(self._nwLangPath, qmFile)): continue if not qmFile.startswith(fPre) or not qmFile.endswith(fExt): continue diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py index 4f9d4954..97bab0de 100644 --- a/novelwriter/core/coretools.py +++ b/novelwriter/core/coretools.py @@ -24,7 +24,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import shutil import logging import novelwriter @@ -431,8 +430,8 @@ class ProjectBuilder: logger.error("No project path set for the example project") return False - pkgSample = os.path.join(self.mainConf.assetPath, "sample.zip") - if os.path.isfile(pkgSample): + pkgSample = self.mainConf.getAssetPath("sample.zip") + if pkgSample.is_file(): try: shutil.unpack_archive(pkgSample, projPath) except Exception as exc: diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index a0e3ac8d..49df331a 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -698,13 +698,13 @@ class NWProject(QObject): def _loadProjectLocalisation(self): """Load the language data for the current project language. """ - if self._data.language is None or self.mainConf.nwLangPath is None: + if self._data.language is None or self.mainConf._nwLangPath is None: self._langData = {} return False - langFile = Path(self.mainConf.nwLangPath) / f"project_{self._data.language}.json" + langFile = Path(self.mainConf._nwLangPath) / f"project_{self._data.language}.json" if not langFile.is_file(): - langFile = Path(self.mainConf.nwLangPath) / "project_en_GB.json" + langFile = Path(self.mainConf._nwLangPath) / "project_en_GB.json" try: with open(langFile, mode="r", encoding="utf-8") as inFile: diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index 5b3a509e..8d4523b1 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -23,7 +23,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import logging import novelwriter @@ -233,7 +232,7 @@ class GuiAbout(QDialog): def _fillNotesPage(self): """Load the content for the Release Notes page. """ - docPath = os.path.join(self.mainConf.assetPath, "text", "release_notes.htm") + docPath = self.mainConf.getAssetPath("text") / "release_notes.htm" docText = readTextFile(docPath) if docText: self.pageNotes.setHtml(docText) @@ -244,7 +243,7 @@ class GuiAbout(QDialog): def _fillLicensePage(self): """Load the content for the Licence page. """ - docPath = os.path.join(self.mainConf.assetPath, "text", "gplv3_en.htm") + docPath = self.mainConf.getAssetPath("text") / "gplv3_en.htm" docText = readTextFile(docPath) if docText: self.pageLicense.setHtml(docText) diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index a3b7eca0..b4acd6f2 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -26,6 +26,7 @@ along with this program. If not, see . import logging import novelwriter +from pathlib import Path from urllib.parse import urljoin from urllib.request import pathname2url @@ -104,10 +105,11 @@ class GuiMainMenu(QMenuBar): def _openUserManualFile(self): """Open the documentation in PDF format. """ - if self.mainConf.pdfDocs is None: - return False - QDesktopServices.openUrl(QUrl(urljoin("file:", pathname2url(self.mainConf.pdfDocs)))) - return True + if isinstance(self.mainConf.pdfDocs, Path): + QDesktopServices.openUrl( + QUrl(urljoin("file:", pathname2url(str(self.mainConf.pdfDocs)))) + ) + return ## # Menu Builders @@ -881,7 +883,7 @@ class GuiMainMenu(QMenuBar): self.helpMenu.addAction(self.aHelpDocs) # Help > User Manual (PDF) - if self.mainConf.pdfDocs is not None: + if isinstance(self.mainConf.pdfDocs, Path): self.aPdfDocs = QAction(self.tr("User Manual (PDF)"), self) self.aPdfDocs.setShortcut("Shift+F1") self.aPdfDocs.triggered.connect(self._openUserManualFile) diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index fc8a7f49..061c8b5c 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -24,12 +24,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import logging import novelwriter from math import ceil -from pathlib import Path from PyQt5.QtCore import Qt from PyQt5.QtWidgets import qApp @@ -120,9 +118,8 @@ class GuiTheme: self._availThemes = {} self._availSyntax = {} - self._listConf(self._availSyntax, os.path.join(self.mainConf.assetPath, "syntax")) - self._listConf(self._availThemes, os.path.join(self.mainConf.assetPath, "themes")) - + self._listConf(self._availSyntax, self.mainConf.getAssetPath("syntax")) + self._listConf(self._availThemes, self.mainConf.getAssetPath("themes")) self._listConf(self._availSyntax, self.mainConf.getDataPath("syntax")) self._listConf(self._availThemes, self.mainConf.getDataPath("themes")) @@ -380,7 +377,6 @@ class GuiTheme: def _listConf(self, targetDict, checkDir): """Scan for theme config files and populate the dictionary. """ - checkDir = Path(checkDir) if not checkDir.is_dir(): return False @@ -476,7 +472,7 @@ class GuiIcons: self._confName = "icons.conf" # Icon Theme Path - self._iconPath = os.path.join(self.mainConf.assetPath, "icons") + self._iconPath = self.mainConf.getAssetPath("icons") # Icon Theme Meta self.themeName = "" @@ -499,12 +495,12 @@ class GuiIcons: update functions for the classes where they're used. """ self._themeMap = {} - themePath = os.path.join(self.mainConf.assetPath, "icons", iconTheme) - if not os.path.isdir(themePath): + themePath = self._iconPath / iconTheme + if not themePath.is_dir(): logger.warning("No icons loaded for '%s'", iconTheme) return False - themeConf = os.path.join(themePath, self._confName) + themeConf = themePath / self._confName logger.info("Loading icon theme '%s'", iconTheme) # Config File @@ -535,8 +531,8 @@ class GuiIcons: if iconName not in self.ICON_KEYS: logger.error("Unknown icon name '%s' in config file", iconName) else: - iconPath = os.path.join(themePath, iconFile) - if os.path.isfile(iconPath): + iconPath = themePath / iconFile + if iconPath.is_file(): self._themeMap[iconName] = iconPath logger.debug("Icon slot '%s' using file '%s'", iconName, iconFile) else: @@ -572,18 +568,16 @@ class GuiIcons: if decoKey in self._themeMap: imgPath = self._themeMap[decoKey] elif decoKey in self.IMAGE_MAP: - imgPath = os.path.join( - self.mainConf.assetPath, "images", self.IMAGE_MAP[decoKey] - ) + imgPath = self.mainConf.getAssetPath("images") / self.IMAGE_MAP[decoKey] else: logger.error("Decoration with name '%s' does not exist", decoKey) return QPixmap() - if not os.path.isfile(imgPath): + if not imgPath.is_file(): logger.error("Asset not found: %s", imgPath) return QPixmap() - theDeco = QPixmap(imgPath) + theDeco = QPixmap(str(imgPath)) if pxW is not None and pxH is not None: return theDeco.scaled(pxW, pxH, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) elif pxW is None and pxH is not None: @@ -667,15 +661,14 @@ class GuiIcons: # If we just want the app icons, return right away if iconKey == "novelwriter": - return QIcon(os.path.join(self._iconPath, "novelwriter.svg")) + return QIcon(str(self._iconPath / "novelwriter.svg")) elif iconKey == "proj_nwx": - return QIcon(os.path.join(self._iconPath, "x-novelwriter-project.svg")) + return QIcon(str(self._iconPath / "x-novelwriter-project.svg")) # Otherwise, we load from the theme folder if iconKey in self._themeMap: - relPath = os.path.relpath(self._themeMap[iconKey], self._iconPath) - logger.debug("Loading: %s", relPath) - return QIcon(self._themeMap[iconKey]) + logger.debug("Loading: %s", self._themeMap[iconKey].name) + return QIcon(str(self._themeMap[iconKey])) # If we didn't find one, give up and return an empty icon logger.warning("Did not load an icon for '%s'", iconKey) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 98471d8d..ae918d9a 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -29,6 +29,7 @@ import novelwriter from enum import Enum from time import time +from pathlib import Path from datetime import datetime from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot @@ -95,7 +96,11 @@ class GuiMain(QMainWindow): # Prepare Main Window self.resize(*self.mainConf.getWinSize()) self._updateWindowTitle() - self.setWindowIcon(QIcon(self.mainConf.appIcon)) + + nwIcon = self.mainConf.getAssetPath("icons") / "novelwriter.svg" + self.nwIcon = QIcon(str(nwIcon)) if nwIcon.is_file() else QIcon() + self.setWindowIcon(self.nwIcon) + qApp.setWindowIcon(self.nwIcon) # Build the GUI # ============= @@ -1362,7 +1367,7 @@ class GuiMain(QMainWindow): # Help self.addAction(self.mainMenu.aHelpDocs) - if self.mainConf.pdfDocs is not None: + if isinstance(self.mainConf.pdfDocs, Path): self.addAction(self.mainMenu.aPdfDocs) return True diff --git a/novelwriter/tools/lipsum.py b/novelwriter/tools/lipsum.py index 976732dd..002f7769 100644 --- a/novelwriter/tools/lipsum.py +++ b/novelwriter/tools/lipsum.py @@ -23,7 +23,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import random import logging import novelwriter @@ -120,7 +119,7 @@ class GuiLipsum(QDialog): def _doInsert(self): """Load the text and insert it in the open document. """ - lipsumFile = os.path.join(self.mainConf.assetPath, "text", "lipsum.txt") + lipsumFile = self.mainConf.getAssetPath("text") / "lipsum.txt" lipsumText = readTextFile(lipsumFile).splitlines() if self.randSwitch.isChecked(): diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py index 42aa9b14..c0f5fe79 100644 --- a/tests/test_base/test_base_config.py +++ b/tests/test_base/test_base_config.py @@ -159,14 +159,14 @@ def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir) assert tstConf._confPath == tmpDir assert tstConf._dataPath == tmpDir - appRoot = tstConf.appRoot + appRoot = tstConf._appRoot mp.setattr("os.path.isfile", lambda *a: True) tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir) assert tstConf._confPath == tmpDir assert tstConf._dataPath == tmpDir - assert tstConf.appRoot == os.path.dirname(appRoot) - assert tstConf.appPath == os.path.dirname(appRoot) + assert tstConf._appRoot == os.path.dirname(appRoot) + assert tstConf._appPath == os.path.dirname(appRoot) assert tstConf.loadConfig() is True assert tstConf.saveConfig() is True @@ -199,7 +199,7 @@ def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): i18nDir = os.path.join(fncDir, "i18n") os.mkdir(i18nDir) os.mkdir(os.path.join(i18nDir, "stuff")) - tstConf.nwLangPath = i18nDir + tstConf._nwLangPath = i18nDir copyfile(os.path.join(filesDir, "nw_en_GB.qm"), os.path.join(i18nDir, "nw_en_GB.qm")) writeFile(os.path.join(i18nDir, "nw_en_GB.ts"), "") diff --git a/tests/test_core/test_core_coretools.py b/tests/test_core/test_core_coretools.py index 4fc6c9ec..5398e4f7 100644 --- a/tests/test_core/test_core_coretools.py +++ b/tests/test_core/test_core_coretools.py @@ -372,7 +372,7 @@ def testCoreTools_NewCustomB(monkeypatch, fncDir, outDir, refDir, mockGUI, mockR @pytest.mark.core -def testCoreTools_NewSample(fncDir, tmpConf, mockGUI, tmpDir): +def testCoreTools_NewSample(monkeypatch, fncPath, tmpConf, tmpPath, mockGUI): """Check that we can create a new project can be created from the provided sample project via a zip file. """ @@ -380,7 +380,7 @@ def testCoreTools_NewSample(fncDir, tmpConf, mockGUI, tmpDir): "projName": "Test Sample", "projTitle": "Test Novel", "projAuthors": "Jane Doe\nJohn Doh\n", - "projPath": fncDir, + "projPath": fncPath, "popSample": True, "popMinimal": False, "popCustom": False, @@ -392,9 +392,11 @@ def testCoreTools_NewSample(fncDir, tmpConf, mockGUI, tmpDir): assert projBuild.buildProject({"popSample": True}) is False # Force the lookup path for assets to our temp folder - srcSample = os.path.abspath(os.path.join(tmpConf.appRoot, "sample")) - dstSample = os.path.join(tmpDir, "sample.zip") - tmpConf.assetPath = tmpDir + srcSample = tmpConf._appRoot / "sample" + dstSample = tmpPath / "sample.zip" + monkeypatch.setattr( + "novelwriter.config.Config.getAssetPath", lambda *a: tmpPath / "sample.zip" + ) # Cannot extract when the zip does not exist assert projBuild.buildProject(projData) is False diff --git a/tests/test_dialogs/test_dlg_about.py b/tests/test_dialogs/test_dlg_about.py index 63de9039..b4357051 100644 --- a/tests/test_dialogs/test_dlg_about.py +++ b/tests/test_dialogs/test_dlg_about.py @@ -21,6 +21,8 @@ along with this program. If not, see . import pytest +from pathlib import Path + from tools import getGuiItem from PyQt5.QtWidgets import QAction, QMessageBox @@ -29,7 +31,7 @@ from novelwriter.dialogs.about import GuiAbout @pytest.mark.gui -def testDlgAbout_NWDialog(qtbot, nwGUI): +def testDlgAbout_NWDialog(qtbot, monkeypatch, nwGUI): """Test the novelWriter about dialogs. """ # NW About @@ -45,13 +47,12 @@ def testDlgAbout_NWDialog(qtbot, nwGUI): assert msgAbout.pageNotes.document().characterCount() > 100 assert msgAbout.pageLicense.document().characterCount() > 100 - msgAbout.mainConf.assetPath = "whatever" - - msgAbout._fillNotesPage() - assert msgAbout.pageNotes.toPlainText() == "Error loading release notes text ..." - - msgAbout._fillLicensePage() - assert msgAbout.pageLicense.toPlainText() == "Error loading licence text ..." + with monkeypatch.context() as mp: + mp.setattr("novelwriter.config.Config.getAssetPath", lambda *a: Path("whatever")) + msgAbout._fillNotesPage() + assert msgAbout.pageNotes.toPlainText() == "Error loading release notes text ..." + msgAbout._fillLicensePage() + assert msgAbout.pageLicense.toPlainText() == "Error loading licence text ..." msgAbout.showReleaseNotes() assert msgAbout.tabBox.currentWidget() == msgAbout.pageNotes diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index cd16ff86..df0a1b15 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -19,10 +19,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import shutil import pytest +from pathlib import Path from configparser import ConfigParser from mock import causeOSError @@ -38,7 +38,7 @@ from novelwriter.gui.theme import GuiIcons, GuiTheme @pytest.mark.gui -def testGuiTheme_Main(qtbot, nwGUI, fncDir): +def testGuiTheme_Main(qtbot, nwGUI, fncPath): """Test the theme class init. """ mainTheme: GuiTheme = nwGUI.mainTheme @@ -75,15 +75,15 @@ def testGuiTheme_Main(qtbot, nwGUI, fncDir): # Scan for Themes # =============== - assert mainTheme._listConf({}, "not_a_path") is False + assert mainTheme._listConf({}, Path("not_a_path")) is False - themeOne = os.path.join(fncDir, "themes", "themeone.conf") - themeTwo = os.path.join(fncDir, "themes", "themetwo.conf") + themeOne = fncPath / "themes" / "themeone.conf" + themeTwo = fncPath / "themes" / "themetwo.conf" writeFile(themeOne, "# Stuff") writeFile(themeTwo, "# Stuff") result = {} - assert mainTheme._listConf(result, os.path.join(fncDir, "themes")) is True + assert mainTheme._listConf(result, fncPath / "themes") is True assert result["themeone"] == themeOne assert result["themetwo"] == themeTwo @@ -123,7 +123,7 @@ def testGuiTheme_Main(qtbot, nwGUI, fncDir): @pytest.mark.gui -def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, fncDir): +def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, fncPath): """Test the theme part of the class. """ mainTheme: GuiTheme = nwGUI.mainTheme @@ -132,15 +132,8 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, fncDir): # List Themes # =========== - shutil.copy( - os.path.join(mainConf.assetPath, "themes", "default_dark.conf"), - os.path.join(fncDir, "themes") - ) - shutil.copy( - os.path.join(mainConf.assetPath, "themes", "default.conf"), - os.path.join(fncDir, "themes") - ) - writeFile(os.path.join(fncDir, "themes", "default.qss"), "/* Stuff */") + shutil.copy(mainConf.getAssetPath("themes") / "default_dark.conf", fncPath / "themes") + shutil.copy(mainConf.getAssetPath("themes") / "default.conf", fncPath / "themes") # Block the reading of the files with monkeypatch.context() as mp: @@ -197,7 +190,7 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, fncDir): @pytest.mark.gui -def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI, fncDir): +def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI, fncPath): """Test the syntax part of the class. """ mainTheme: GuiTheme = nwGUI.mainTheme @@ -206,14 +199,8 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI, fncDir): # List Themes # =========== - shutil.copy( - os.path.join(mainConf.assetPath, "syntax", "default_dark.conf"), - os.path.join(fncDir, "syntax") - ) - shutil.copy( - os.path.join(mainConf.assetPath, "syntax", "default_light.conf"), - os.path.join(fncDir, "syntax") - ) + shutil.copy(mainConf.getAssetPath("syntax") / "default_dark.conf", fncPath / "syntax") + shutil.copy(mainConf.getAssetPath("syntax") / "default_light.conf", fncPath / "syntax") # Block the reading of the files with monkeypatch.context() as mp: @@ -270,11 +257,10 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI, fncDir): @pytest.mark.gui -def testGuiTheme_Icons(qtbot, caplog, monkeypatch, nwGUI, fncDir): +def testGuiTheme_Icons(qtbot, caplog, monkeypatch, nwGUI, fncPath): """Test the icon cache class. """ iconCache: GuiIcons = nwGUI.mainTheme.iconCache - mainConf: Config = nwGUI.mainConf # Load Theme # ========== @@ -288,10 +274,11 @@ def testGuiTheme_Icons(qtbot, caplog, monkeypatch, nwGUI, fncDir): assert iconCache.loadTheme("typicons_dark") is False # Load a broken theme file - iconsDir = os.path.join(fncDir, "icons") - os.mkdir(iconsDir) - os.mkdir(os.path.join(iconsDir, "testicons")) - writeFile(os.path.join(iconsDir, "testicons", "icons.conf"), ( + iconsDir = fncPath / "icons" + testIcons = iconsDir / "testicons" + iconsDir.mkdir() + testIcons.mkdir() + writeFile(testIcons / "icons.conf", ( "[Main]\n" "name = Test Icons\n" "\n" @@ -300,15 +287,15 @@ def testGuiTheme_Icons(qtbot, caplog, monkeypatch, nwGUI, fncDir): "stuff = stuff.svg\n" )) - assetPath = mainConf.assetPath - mainConf.assetPath = fncDir + iconPath = iconCache._iconPath + iconCache._iconPath = fncPath / "icons" caplog.clear() assert iconCache.loadTheme("testicons") is True assert "Unknown icon name 'stuff' in config file" in caplog.text assert "Icon file 'add.svg' not in theme folder" in caplog.text - mainConf.assetPath = assetPath + iconCache._iconPath = iconPath # Load working theme file assert iconCache.loadTheme("typicons_dark") is True @@ -327,7 +314,7 @@ def testGuiTheme_Icons(qtbot, caplog, monkeypatch, nwGUI, fncDir): # Fail finding the file with monkeypatch.context() as mp: - mp.setattr("os.path.isfile", lambda *a: False) + mp.setattr("pathlib.Path.is_file", lambda *a: False) qPix = iconCache.loadDecoration("wiz-back") assert qPix.isNull() is True From 47b57172ebb3b833c437d493a34b5c868a26a5da Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 9 Nov 2022 18:21:40 +0100 Subject: [PATCH 5/9] Change lastPath to a Path object --- novelwriter/config.py | 49 +++++++++++---------- novelwriter/guimain.py | 4 +- novelwriter/tools/build.py | 9 +--- novelwriter/tools/projwizard.py | 7 +-- novelwriter/tools/writingstats.py | 11 +---- tests/conftest.py | 10 ++--- tests/test_base/test_base_config.py | 3 +- tests/test_dialogs/test_dlg_preferences.py | 4 +- tests/test_dialogs/test_dlg_projdetails.py | 1 - tests/test_gui/test_gui_outline.py | 1 - tests/test_tools/test_tools_build.py | 12 ++--- tests/test_tools/test_tools_projwizard.py | 2 - tests/test_tools/test_tools_writingstats.py | 3 -- 13 files changed, 45 insertions(+), 71 deletions(-) diff --git a/novelwriter/config.py b/novelwriter/config.py index ef342f4d..5b4cfebd 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -64,26 +64,20 @@ class Config: self._confPath = confRoot.absolute() / self.appHandle # The user config location self._dataPath = dataRoot.absolute() / self.appHandle # The user data location + self._lastPath = Path.home().absolute() # The user's last used path - if hasattr(sys, "_MEIPASS"): - self._appPath = Path(sys._MEIPASS).absolute() - else: - self._appPath = Path(__file__).parent.absolute() - + self._appPath = Path(__file__).parent.absolute() self._appRoot = self._appPath.parent if self._appRoot.is_file(): # novelWriter is packaged as a single file self._appRoot = self._appRoot.parent self._appPath = self._appRoot - self.cmdOpen = None # Path from command line for project to be opened on launch - self.lastPath = None # The last user-selected folder (browse dialogs) - self.pdfDocs = None # The location of the PDF manual, if it exists - # Runtime Settings and Variables self.hasError = False # True if the config class encountered an error self.errData = [] # List of error messages self.confChanged = False # True whenever the config has chenged, false after save + self.cmdOpen = None # Path from command line for project to be opened on launch # Localisation Info self._qLocal = QLocale.system() @@ -91,6 +85,10 @@ class Config: self._qtLangPath = QLibraryInfo.location(QLibraryInfo.TranslationsPath) self._nwLangPath = str(self._appPath / "assets" / "i18n") + # PDF Manual + pdfDocs = self._appPath / "assets" / "manual.pdf" + self.pdfDocs = pdfDocs if pdfDocs.is_file() else None + # User Settings # ============= @@ -277,6 +275,13 @@ class Config: return self._appPath / "assets" / target return self._appPath / "assets" + def getLastPath(self): + """Return the last path used by the user, but ensure it exists. + """ + if self._lastPath.is_dir(): + return self._lastPath + return Path.home().absolute() + ## # Config Actions ## @@ -298,9 +303,8 @@ class Config: logger.debug("Data Path: %s", self._dataPath) logger.debug("App Root: %s", self._appRoot) logger.debug("App Path: %s", self._appPath) - - self.lastPath = os.path.expanduser("~") - logger.debug("Last path: %s", self.lastPath) + logger.debug("Last Path: %s", self._lastPath) + logger.debug("PDF Manual: %s", self.pdfDocs) # If the config and data folders don't not exist, create them # This assumes that the os config and data folders exist @@ -329,12 +333,6 @@ class Config: if not self.spellLanguage: self.spellLanguage = "en" - # Look for a PDF version of the manual - pdfDocs = self._appPath / "assets" / "manual.pdf" - if pdfDocs.is_file(): - logger.debug("Found PDF manual: %s", pdfDocs) - self.pdfDocs = pdfDocs - logger.debug("Config initialisation complete") return True @@ -495,7 +493,7 @@ class Config: # Path cnfSec = "Path" - self.lastPath = theConf.rdStr(cnfSec, "lastpath", self.lastPath) + self._lastPath = Path(theConf.rdStr(cnfSec, "lastpath", self._lastPath)) # Check Certain Values for None self.spellLanguage = self._checkNone(self.spellLanguage) @@ -605,7 +603,7 @@ class Config: } theConf["Path"] = { - "lastpath": str(self.lastPath), + "lastpath": str(self._lastPath), } # Write config file @@ -698,10 +696,13 @@ class Config: def setLastPath(self, lastPath): """Set the last used path (by the user). """ - if lastPath is None or lastPath == "": - self.lastPath = "" - else: - self.lastPath = os.path.dirname(lastPath) + if isinstance(lastPath, str): + lastPath = Path(lastPath) + if isinstance(lastPath, Path): + if lastPath.is_file(): + self._lastPath = lastPath.parent + elif lastPath.is_dir(): + self._lastPath = lastPath return True def setWinSize(self, newWidth, newHeight): diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index ae918d9a..90141c03 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -698,7 +698,7 @@ class GuiMain(QMainWindow): logger.error("No project open") return False - lastPath = self.mainConf.lastPath + lastPath = self.mainConf.getLastPath() extFilter = [ self.tr("Text files ({0})").format("*.txt"), self.tr("Markdown files ({0})").format("*.md"), @@ -706,7 +706,7 @@ class GuiMain(QMainWindow): self.tr("All files ({0})").format("*"), ] loadFile, _ = QFileDialog.getOpenFileName( - self, self.tr("Import File"), lastPath, filter=";;".join(extFilter) + self, self.tr("Import File"), str(lastPath), filter=";;".join(extFilter) ) if not loadFile: return False diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index 78089abf..198ae15e 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -23,7 +23,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import json import logging import novelwriter @@ -891,13 +890,9 @@ class GuiBuildNovel(QDialog): cleanName = makeFileNameSafe(self.theProject.data.name) fileName = "%s.%s" % (cleanName, fileExt) - saveDir = self.mainConf.lastPath - if not os.path.isdir(saveDir): - saveDir = os.path.expanduser("~") - - savePath = os.path.join(saveDir, fileName) + savePath = self.mainConf.getLastPath() / fileName savePath, _ = QFileDialog.getSaveFileName( - self, self.tr("Save Document As"), savePath + self, self.tr("Save Document As"), str(savePath) ) if not savePath: return False diff --git a/novelwriter/tools/projwizard.py b/novelwriter/tools/projwizard.py index 94cab911..bcc88484 100644 --- a/novelwriter/tools/projwizard.py +++ b/novelwriter/tools/projwizard.py @@ -236,12 +236,9 @@ class ProjWizardFolderPage(QWizardPage): def _doBrowse(self): """Select a project folder. """ - lastPath = self.mainConf.lastPath - if not os.path.isdir(lastPath): - lastPath = "" - + lastPath = self.mainConf.getLastPath() projDir = QFileDialog.getExistingDirectory( - self, self.tr("Select Project Folder"), lastPath, options=QFileDialog.ShowDirsOnly + self, self.tr("Select Project Folder"), str(lastPath), options=QFileDialog.ShowDirsOnly ) if projDir: projName = self.field("projName") diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index 7015c992..efa3a6ca 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -23,7 +23,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import json import logging import novelwriter @@ -363,15 +362,9 @@ class GuiWritingStats(QDialog): return False # Generate the file name - saveDir = self.mainConf.lastPath - if not os.path.isdir(saveDir): - saveDir = os.path.expanduser("~") - - fileName = "sessionStats.%s" % fileExt - savePath = os.path.join(saveDir, fileName) - + savePath = self.mainConf.getLastPath() / f"sessionStats.{fileExt}" savePath, _ = QFileDialog.getSaveFileName( - self, self.tr("Save Data As"), savePath, "%s (*.%s)" % (textFmt, fileExt) + self, self.tr("Save Data As"), str(savePath), "%s (*.%s)" % (textFmt, fileExt) ) if not savePath: return False diff --git a/tests/conftest.py b/tests/conftest.py index 93a024c8..696d416e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -166,7 +166,7 @@ def tmpConf(tmpPath): confFile.unlink() theConf = Config() theConf.initConfig(tmpPath, tmpPath) - theConf.setLastPath("") + theConf.setLastPath(tmpPath) theConf.guiLang = "en_GB" return theConf @@ -180,7 +180,7 @@ def fncConf(fncPath): confFile.unlink() theConf = Config() theConf.initConfig(fncPath, fncPath) - theConf.setLastPath("") + theConf.setLastPath(fncPath) theConf.guiLang = "en_GB" return theConf @@ -196,7 +196,7 @@ def mockGUI(monkeypatch, tmpConf): @pytest.fixture(scope="function") -def nwGUI(qtbot, monkeypatch, fncDir, fncConf): +def nwGUI(qtbot, monkeypatch, fncPath, fncConf): """Create an instance of the novelWriter GUI. """ monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Ok) @@ -205,12 +205,12 @@ def nwGUI(qtbot, monkeypatch, fncDir, fncConf): monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr("novelwriter.CONFIG", fncConf) - nwGUI = novelwriter.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % fncDir]) + nwGUI = novelwriter.main(["--testmode", f"--config={fncPath}", f"--data={fncPath}"]) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.wait(20) - nwGUI.mainConf.lastPath = fncDir + nwGUI.mainConf.setLastPath(fncPath) yield nwGUI diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py index c0f5fe79..36f0eb79 100644 --- a/tests/test_base/test_base_config.py +++ b/tests/test_base/test_base_config.py @@ -444,7 +444,8 @@ def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir): assert tmpConf.confChanged is False copyfile(confFile, testFile) - assert cmpFiles(testFile, compFile, ignoreStart=("timestamp", "lastnotes", "guilang")) + ignore = ("timestamp", "lastnotes", "guilang", "lastpath") + assert cmpFiles(testFile, compFile, ignoreStart=ignore) # END Test testBaseConfig_SettersGetters diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index 55d3c056..fbf9af9b 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -22,6 +22,7 @@ along with this program. If not, see . import pytest from shutil import copyfile + from tools import cmpFiles, getGuiItem from PyQt5.QtCore import Qt @@ -215,7 +216,6 @@ def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, fncPath, tstPaths): nwPrefs._doClose() assert theConf.confChanged - theConf.lastPath = "" assert nwGUI.mainConf.saveConfig() projFile = fncPath / "novelwriter.conf" @@ -225,7 +225,7 @@ def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, fncPath, tstPaths): ignTuple = ( "timestamp", "guifont", "lastnotes", "guilang", "geometry", "preferences", "projcols", "mainpane", "docpane", "viewpane", - "outlinepane", "textfont", "textsize" + "outlinepane", "textfont", "textsize", "lastpath" ) assert cmpFiles(testFile, compFile, ignoreStart=ignTuple) diff --git a/tests/test_dialogs/test_dlg_projdetails.py b/tests/test_dialogs/test_dlg_projdetails.py index c7d86670..e8d6053c 100644 --- a/tests/test_dialogs/test_dlg_projdetails.py +++ b/tests/test_dialogs/test_dlg_projdetails.py @@ -38,7 +38,6 @@ def testDlgProjDetails_Dialog(qtbot, nwGUI, nwLipsum): qtbot.wait(100) # Open the Writing Stats dialog - nwGUI.mainConf.lastPath = "" nwGUI.mainMenu.aProjectDetails.activate(QAction.Trigger) qtbot.waitUntil(lambda: getGuiItem("GuiProjectDetails") is not None, timeout=1000) diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 91467683..77d9e739 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -156,7 +156,6 @@ def testGuiOutline_Content(qtbot, nwGUI, nwLipsum): """Test the outline view. """ assert nwGUI.openProject(nwLipsum) - nwGUI.mainConf.lastPath = nwLipsum nwGUI.rebuildIndex() nwGUI._changeView(nwView.OUTLINE) diff --git a/tests/test_tools/test_tools_build.py b/tests/test_tools/test_tools_build.py index e0088a1c..5cb93229 100644 --- a/tests/test_tools/test_tools_build.py +++ b/tests/test_tools/test_tools_build.py @@ -23,6 +23,8 @@ import pytest import os from shutil import copyfile +from pathlib import Path + from tools import cmpFiles, getGuiItem from PyQt5.QtCore import Qt @@ -61,21 +63,13 @@ def testToolBuild_Main(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir): # Invalid file format assert not nwBuild._saveDocument(-1) - # Non-existent path - with monkeypatch.context() as mp: - mp.setattr("os.path.expanduser", lambda *a, **k: nwLipsum) - assert nwGUI.mainConf.lastPath != nwLipsum - nwGUI.mainConf.lastPath = "no_such_path" - assert nwBuild._saveDocument(nwBuild.FMT_NWD) - assert nwGUI.mainConf.lastPath == nwLipsum - # No path selected with monkeypatch.context() as mp: mp.setattr(QFileDialog, "getSaveFileName", lambda *a, **k: ("", "")) assert not nwBuild._saveDocument(nwBuild.FMT_NWD) # Default Settings - nwGUI.mainConf.lastPath = nwLipsum + nwGUI.mainConf._lastPath = Path(nwLipsum) qtbot.mouseClick(nwBuild.buildNovel, Qt.LeftButton) assert nwBuild._saveDocument(nwBuild.FMT_NWD) diff --git a/tests/test_tools/test_tools_projwizard.py b/tests/test_tools/test_tools_projwizard.py index 5d598e6d..ed9d8a58 100644 --- a/tests/test_tools/test_tools_projwizard.py +++ b/tests/test_tools/test_tools_projwizard.py @@ -67,7 +67,6 @@ def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, fncProj): # Test the Wizard Launching # ========================= - nwGUI.mainConf.lastPath = " " monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *a: None) result = nwGUI.showNewProjectDialog() @@ -102,7 +101,6 @@ def testToolProjectWizard_Run(qtbot, monkeypatch, nwGUI, fncDir, prjType): """ monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *a: None) - nwGUI.mainConf.lastPath = " " nwWiz = GuiProjectWizard(nwGUI) nwWiz.show() qtbot.addWidget(nwWiz) diff --git a/tests/test_tools/test_tools_writingstats.py b/tests/test_tools/test_tools_writingstats.py index a4ba1963..60f59750 100644 --- a/tests/test_tools/test_tools_writingstats.py +++ b/tests/test_tools/test_tools_writingstats.py @@ -44,7 +44,6 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj): sessFile = os.path.join(fncProj, "meta", nwFiles.SESS_STATS) # Open the Writing Stats dialog - nwGUI.mainConf.lastPath = "" nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger) qtbot.waitUntil(lambda: getGuiItem("GuiWritingStats") is not None, timeout=1000) @@ -135,8 +134,6 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj): assert sessLog._saveData(sessLog.FMT_JSON) qtbot.wait(100) - assert nwGUI.mainConf.lastPath == fncDir - # Check the exported files jsonStats = os.path.join(fncDir, "sessionStats.json") with open(jsonStats, mode="r", encoding="utf-8") as inFile: From 8721d53ab2364d1d125b94a3cd1e3d5075a40630 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 9 Nov 2022 18:47:50 +0100 Subject: [PATCH 6/9] Clean up the config class names and methods --- novelwriter/config.py | 64 +++++++++++--------------- novelwriter/core/coretools.py | 2 +- novelwriter/dialogs/about.py | 4 +- novelwriter/gui/theme.py | 12 ++--- novelwriter/guimain.py | 4 +- novelwriter/tools/build.py | 2 +- novelwriter/tools/lipsum.py | 2 +- novelwriter/tools/projwizard.py | 2 +- novelwriter/tools/writingstats.py | 2 +- tests/test_base/test_base_config.py | 61 ++++++++++++------------ tests/test_core/test_core_coretools.py | 2 +- tests/test_dialogs/test_dlg_about.py | 2 +- tests/test_gui/test_gui_theme.py | 8 ++-- 13 files changed, 81 insertions(+), 86 deletions(-) diff --git a/novelwriter/config.py b/novelwriter/config.py index 5b4cfebd..af523dc5 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -74,10 +74,10 @@ class Config: self._appPath = self._appRoot # Runtime Settings and Variables - self.hasError = False # True if the config class encountered an error - self.errData = [] # List of error messages + self.hasError = False # True if the config class encountered an error + self.errData = [] # List of error messages self.confChanged = False # True whenever the config has chenged, false after save - self.cmdOpen = None # Path from command line for project to be opened on launch + self.cmdOpen = None # Path from command line for project to be opened on launch # Localisation Info self._qLocal = QLocale.system() @@ -171,7 +171,7 @@ class Config: self.fmtPadThin = False # Spell Checking Settings - self.spellLanguage = None + self.spellLanguage = "en" # Search Bar Switches self.searchCase = False @@ -261,21 +261,21 @@ class Config: """ return int(theSize/self.guiScale) - def getDataPath(self, target=None): + def dataPath(self, target=None): """Return a path in the data folder. """ if isinstance(target, str): return self._dataPath / target return self._dataPath - def getAssetPath(self, target=None): + def assetPath(self, target=None): """Return a path in the assets folder. """ if isinstance(target, str): return self._appPath / "assets" / target return self._appPath / "assets" - def getLastPath(self): + def lastPath(self): """Return the last path used by the user, but ensure it exists. """ if self._lastPath.is_dir(): @@ -294,7 +294,6 @@ class Config: if isinstance(confPath, (str, Path)): logger.info("Setting config from alternative path: %s", confPath) self._confPath = Path(confPath) - if isinstance(dataPath, (str, Path)): logger.info("Setting data path from alternative path: %s", dataPath) self._dataPath = Path(dataPath) @@ -306,33 +305,25 @@ class Config: logger.debug("Last Path: %s", self._lastPath) logger.debug("PDF Manual: %s", self.pdfDocs) - # If the config and data folders don't not exist, create them + # If the config and data folders don't exist, create them # This assumes that the os config and data folders exist self._confPath.mkdir(exist_ok=True) self._dataPath.mkdir(exist_ok=True) - # We don't error on these failing since they are not essential + # Also create the syntax and themes folders if possible if self._dataPath.is_dir(): (self._dataPath / "syntax").mkdir(exist_ok=True) (self._dataPath / "themes").mkdir(exist_ok=True) - # Check if config file exists + # Check if config file exists, and load it. If not, we save defaults if (self._confPath / nwFiles.CONF_FILE).is_file(): - # If it exists, load it self.loadConfig() else: - # If it does not exist, save a copy of the default values self.saveConfig() - # Load recent projects cache self.loadRecentCache() - - # Check the availability of optional packages self._checkOptionalPackages() - if not self.spellLanguage: - self.spellLanguage = "en" - logger.debug("Config initialisation complete") return True @@ -694,16 +685,17 @@ class Config: ## def setLastPath(self, lastPath): - """Set the last used path (by the user). + """Set the last used path. Only the folder is saved, so if the + path is not a folder, the parent of the path is used instead. """ - if isinstance(lastPath, str): + if isinstance(lastPath, (str, Path)): lastPath = Path(lastPath) - if isinstance(lastPath, Path): - if lastPath.is_file(): - self._lastPath = lastPath.parent - elif lastPath.is_dir(): + if not lastPath.is_dir(): + lastPath = lastPath.parent + if lastPath.is_dir(): self._lastPath = lastPath - return True + logger.debug("Last path updated: %s" % self._lastPath) + return def setWinSize(self, newWidth, newHeight): """Set the size of the main window, but only if the change is @@ -719,7 +711,7 @@ class Config: if abs(self.winGeometry[1] - newHeight) > 5: self.winGeometry[1] = newHeight self.confChanged = True - return True + return def setPreferencesSize(self, newWidth, newHeight): """Sat the size of the Preferences dialog window. @@ -727,63 +719,63 @@ class Config: self.prefGeometry[0] = int(newWidth/self.guiScale) self.prefGeometry[1] = int(newHeight/self.guiScale) self.confChanged = True - return True + return def setProjColWidths(self, colWidths): """Set the column widths of the Load Project dialog. """ self.projColWidth = [int(x/self.guiScale) for x in colWidths] self.confChanged = True - return True + return def setMainPanePos(self, panePos): """Set the position of the main GUI splitter. """ self.mainPanePos = [int(x/self.guiScale) for x in panePos] self.confChanged = True - return True + return def setDocPanePos(self, panePos): """Set the position of the main editor/viewer splitter. """ self.docPanePos = [int(x/self.guiScale) for x in panePos] self.confChanged = True - return True + return def setViewPanePos(self, panePos): """Set the position of the viewer meta data splitter. """ self.viewPanePos = [int(x/self.guiScale) for x in panePos] self.confChanged = True - return True + return def setOutlinePanePos(self, panePos): """Set the position of the outline details splitter. """ self.outlnPanePos = [int(x/self.guiScale) for x in panePos] self.confChanged = True - return True + return def setShowRefPanel(self, checkState): """Set the visibility state of the reference panel. """ self.showRefPanel = checkState self.confChanged = True - return self.showRefPanel + return def setViewComments(self, viewState): """Set the visibility state of comments in the viewer. """ self.viewComments = viewState self.confChanged = True - return self.viewComments + return def setViewSynopsis(self, viewState): """Set the visibility state of synopsis comments in the viewer. """ self.viewSynopsis = viewState self.confChanged = True - return self.viewSynopsis + return ## # Default Setters diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py index 97bab0de..ff002935 100644 --- a/novelwriter/core/coretools.py +++ b/novelwriter/core/coretools.py @@ -430,7 +430,7 @@ class ProjectBuilder: logger.error("No project path set for the example project") return False - pkgSample = self.mainConf.getAssetPath("sample.zip") + pkgSample = self.mainConf.assetPath("sample.zip") if pkgSample.is_file(): try: shutil.unpack_archive(pkgSample, projPath) diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index 8d4523b1..11617a33 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -232,7 +232,7 @@ class GuiAbout(QDialog): def _fillNotesPage(self): """Load the content for the Release Notes page. """ - docPath = self.mainConf.getAssetPath("text") / "release_notes.htm" + docPath = self.mainConf.assetPath("text") / "release_notes.htm" docText = readTextFile(docPath) if docText: self.pageNotes.setHtml(docText) @@ -243,7 +243,7 @@ class GuiAbout(QDialog): def _fillLicensePage(self): """Load the content for the Licence page. """ - docPath = self.mainConf.getAssetPath("text") / "gplv3_en.htm" + docPath = self.mainConf.assetPath("text") / "gplv3_en.htm" docText = readTextFile(docPath) if docText: self.pageLicense.setHtml(docText) diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 061c8b5c..2d69806c 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -118,10 +118,10 @@ class GuiTheme: self._availThemes = {} self._availSyntax = {} - self._listConf(self._availSyntax, self.mainConf.getAssetPath("syntax")) - self._listConf(self._availThemes, self.mainConf.getAssetPath("themes")) - self._listConf(self._availSyntax, self.mainConf.getDataPath("syntax")) - self._listConf(self._availThemes, self.mainConf.getDataPath("themes")) + self._listConf(self._availSyntax, self.mainConf.assetPath("syntax")) + self._listConf(self._availThemes, self.mainConf.assetPath("themes")) + self._listConf(self._availSyntax, self.mainConf.dataPath("syntax")) + self._listConf(self._availThemes, self.mainConf.dataPath("themes")) self.loadTheme() self.loadSyntax() @@ -472,7 +472,7 @@ class GuiIcons: self._confName = "icons.conf" # Icon Theme Path - self._iconPath = self.mainConf.getAssetPath("icons") + self._iconPath = self.mainConf.assetPath("icons") # Icon Theme Meta self.themeName = "" @@ -568,7 +568,7 @@ class GuiIcons: if decoKey in self._themeMap: imgPath = self._themeMap[decoKey] elif decoKey in self.IMAGE_MAP: - imgPath = self.mainConf.getAssetPath("images") / self.IMAGE_MAP[decoKey] + imgPath = self.mainConf.assetPath("images") / self.IMAGE_MAP[decoKey] else: logger.error("Decoration with name '%s' does not exist", decoKey) return QPixmap() diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 90141c03..fb77ca8a 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -97,7 +97,7 @@ class GuiMain(QMainWindow): self.resize(*self.mainConf.getWinSize()) self._updateWindowTitle() - nwIcon = self.mainConf.getAssetPath("icons") / "novelwriter.svg" + nwIcon = self.mainConf.assetPath("icons") / "novelwriter.svg" self.nwIcon = QIcon(str(nwIcon)) if nwIcon.is_file() else QIcon() self.setWindowIcon(self.nwIcon) qApp.setWindowIcon(self.nwIcon) @@ -698,7 +698,7 @@ class GuiMain(QMainWindow): logger.error("No project open") return False - lastPath = self.mainConf.getLastPath() + lastPath = self.mainConf.lastPath() extFilter = [ self.tr("Text files ({0})").format("*.txt"), self.tr("Markdown files ({0})").format("*.md"), diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index 198ae15e..2935c556 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -890,7 +890,7 @@ class GuiBuildNovel(QDialog): cleanName = makeFileNameSafe(self.theProject.data.name) fileName = "%s.%s" % (cleanName, fileExt) - savePath = self.mainConf.getLastPath() / fileName + savePath = self.mainConf.lastPath() / fileName savePath, _ = QFileDialog.getSaveFileName( self, self.tr("Save Document As"), str(savePath) ) diff --git a/novelwriter/tools/lipsum.py b/novelwriter/tools/lipsum.py index 002f7769..def306c5 100644 --- a/novelwriter/tools/lipsum.py +++ b/novelwriter/tools/lipsum.py @@ -119,7 +119,7 @@ class GuiLipsum(QDialog): def _doInsert(self): """Load the text and insert it in the open document. """ - lipsumFile = self.mainConf.getAssetPath("text") / "lipsum.txt" + lipsumFile = self.mainConf.assetPath("text") / "lipsum.txt" lipsumText = readTextFile(lipsumFile).splitlines() if self.randSwitch.isChecked(): diff --git a/novelwriter/tools/projwizard.py b/novelwriter/tools/projwizard.py index bcc88484..fbd2b8ac 100644 --- a/novelwriter/tools/projwizard.py +++ b/novelwriter/tools/projwizard.py @@ -236,7 +236,7 @@ class ProjWizardFolderPage(QWizardPage): def _doBrowse(self): """Select a project folder. """ - lastPath = self.mainConf.getLastPath() + lastPath = self.mainConf.lastPath() projDir = QFileDialog.getExistingDirectory( self, self.tr("Select Project Folder"), str(lastPath), options=QFileDialog.ShowDirsOnly ) diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index efa3a6ca..5e8606d3 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -362,7 +362,7 @@ class GuiWritingStats(QDialog): return False # Generate the file name - savePath = self.mainConf.getLastPath() / f"sessionStats.{fileExt}" + savePath = self.mainConf.lastPath() / f"sessionStats.{fileExt}" savePath, _ = QFileDialog.getSaveFileName( self, self.tr("Save Data As"), str(savePath), "%s (*.%s)" % (textFmt, fileExt) ) diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py index 36f0eb79..fdca0b8b 100644 --- a/tests/test_base/test_base_config.py +++ b/tests/test_base/test_base_config.py @@ -313,98 +313,98 @@ def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir): # Window Size tmpConf.guiScale = 1.0 - assert tmpConf.setWinSize(1205, 655) - assert not tmpConf.confChanged + tmpConf.setWinSize(1205, 655) + assert tmpConf.confChanged is False tmpConf.guiScale = 2.0 - assert tmpConf.setWinSize(70, 70) + tmpConf.setWinSize(70, 70) assert tmpConf.getWinSize() == [70, 70] assert tmpConf.winGeometry == [35, 35] tmpConf.guiScale = 1.0 - assert tmpConf.setWinSize(70, 70) + tmpConf.setWinSize(70, 70) assert tmpConf.getWinSize() == [70, 70] assert tmpConf.winGeometry == [70, 70] - assert tmpConf.setWinSize(1200, 650) + tmpConf.setWinSize(1200, 650) # Preferences Size tmpConf.guiScale = 2.0 - assert tmpConf.setPreferencesSize(70, 70) + tmpConf.setPreferencesSize(70, 70) assert tmpConf.getPreferencesSize() == [70, 70] assert tmpConf.prefGeometry == [35, 35] tmpConf.guiScale = 1.0 - assert tmpConf.setPreferencesSize(70, 70) + tmpConf.setPreferencesSize(70, 70) assert tmpConf.getPreferencesSize() == [70, 70] assert tmpConf.prefGeometry == [70, 70] - assert tmpConf.setPreferencesSize(700, 615) + tmpConf.setPreferencesSize(700, 615) # Project Settings Tree Columns tmpConf.guiScale = 2.0 - assert tmpConf.setProjColWidths([10, 20, 30]) + tmpConf.setProjColWidths([10, 20, 30]) assert tmpConf.getProjColWidths() == [10, 20, 30] assert tmpConf.projColWidth == [5, 10, 15] tmpConf.guiScale = 1.0 - assert tmpConf.setProjColWidths([10, 20, 30]) + tmpConf.setProjColWidths([10, 20, 30]) assert tmpConf.getProjColWidths() == [10, 20, 30] assert tmpConf.projColWidth == [10, 20, 30] - assert tmpConf.setProjColWidths([200, 60, 140]) + tmpConf.setProjColWidths([200, 60, 140]) # Main Pane Splitter tmpConf.guiScale = 2.0 - assert tmpConf.setMainPanePos([200, 700]) + tmpConf.setMainPanePos([200, 700]) assert tmpConf.getMainPanePos() == [200, 700] assert tmpConf.mainPanePos == [100, 350] tmpConf.guiScale = 1.0 - assert tmpConf.setMainPanePos([200, 700]) + tmpConf.setMainPanePos([200, 700]) assert tmpConf.getMainPanePos() == [200, 700] assert tmpConf.mainPanePos == [200, 700] - assert tmpConf.setMainPanePos([300, 800]) + tmpConf.setMainPanePos([300, 800]) # Doc Pane Splitter tmpConf.guiScale = 2.0 - assert tmpConf.setDocPanePos([300, 300]) + tmpConf.setDocPanePos([300, 300]) assert tmpConf.getDocPanePos() == [300, 300] assert tmpConf.docPanePos == [150, 150] tmpConf.guiScale = 1.0 - assert tmpConf.setDocPanePos([300, 300]) + tmpConf.setDocPanePos([300, 300]) assert tmpConf.getDocPanePos() == [300, 300] assert tmpConf.docPanePos == [300, 300] - assert tmpConf.setDocPanePos([400, 400]) + tmpConf.setDocPanePos([400, 400]) # View Pane Splitter tmpConf.guiScale = 2.0 - assert tmpConf.setViewPanePos([400, 250]) + tmpConf.setViewPanePos([400, 250]) assert tmpConf.getViewPanePos() == [400, 250] assert tmpConf.viewPanePos == [200, 125] tmpConf.guiScale = 1.0 - assert tmpConf.setViewPanePos([400, 250]) + tmpConf.setViewPanePos([400, 250]) assert tmpConf.getViewPanePos() == [400, 250] assert tmpConf.viewPanePos == [400, 250] - assert tmpConf.setViewPanePos([500, 150]) + tmpConf.setViewPanePos([500, 150]) # Outline Pane Splitter tmpConf.guiScale = 2.0 - assert tmpConf.setOutlinePanePos([400, 250]) + tmpConf.setOutlinePanePos([400, 250]) assert tmpConf.getOutlinePanePos() == [400, 250] assert tmpConf.outlnPanePos == [200, 125] tmpConf.guiScale = 1.0 - assert tmpConf.setOutlinePanePos([400, 250]) + tmpConf.setOutlinePanePos([400, 250]) assert tmpConf.getOutlinePanePos() == [400, 250] assert tmpConf.outlnPanePos == [400, 250] - assert tmpConf.setOutlinePanePos([500, 150]) + tmpConf.setOutlinePanePos([500, 150]) # Getters Only # ============ @@ -424,17 +424,20 @@ def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir): # Flag Setters # ============ - assert tmpConf.setShowRefPanel(False) is False + tmpConf.setShowRefPanel(False) assert tmpConf.showRefPanel is False - assert tmpConf.setShowRefPanel(True) is True + tmpConf.setShowRefPanel(True) + assert tmpConf.showRefPanel is True - assert tmpConf.setViewComments(False) is False + tmpConf.setViewComments(False) assert tmpConf.viewComments is False - assert tmpConf.setViewComments(True) is True + tmpConf.setViewComments(True) + assert tmpConf.viewComments is True - assert tmpConf.setViewSynopsis(False) is False + tmpConf.setViewSynopsis(False) assert tmpConf.viewSynopsis is False - assert tmpConf.setViewSynopsis(True) is True + tmpConf.setViewSynopsis(True) + assert tmpConf.viewSynopsis is True # Check Final File # ================ diff --git a/tests/test_core/test_core_coretools.py b/tests/test_core/test_core_coretools.py index 5398e4f7..39d95a7a 100644 --- a/tests/test_core/test_core_coretools.py +++ b/tests/test_core/test_core_coretools.py @@ -395,7 +395,7 @@ def testCoreTools_NewSample(monkeypatch, fncPath, tmpConf, tmpPath, mockGUI): srcSample = tmpConf._appRoot / "sample" dstSample = tmpPath / "sample.zip" monkeypatch.setattr( - "novelwriter.config.Config.getAssetPath", lambda *a: tmpPath / "sample.zip" + "novelwriter.config.Config.assetPath", lambda *a: tmpPath / "sample.zip" ) # Cannot extract when the zip does not exist diff --git a/tests/test_dialogs/test_dlg_about.py b/tests/test_dialogs/test_dlg_about.py index b4357051..1b650ec1 100644 --- a/tests/test_dialogs/test_dlg_about.py +++ b/tests/test_dialogs/test_dlg_about.py @@ -48,7 +48,7 @@ def testDlgAbout_NWDialog(qtbot, monkeypatch, nwGUI): assert msgAbout.pageLicense.document().characterCount() > 100 with monkeypatch.context() as mp: - mp.setattr("novelwriter.config.Config.getAssetPath", lambda *a: Path("whatever")) + mp.setattr("novelwriter.config.Config.assetPath", lambda *a: Path("whatever")) msgAbout._fillNotesPage() assert msgAbout.pageNotes.toPlainText() == "Error loading release notes text ..." msgAbout._fillLicensePage() diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index df0a1b15..42f5c7c5 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -132,8 +132,8 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, fncPath): # List Themes # =========== - shutil.copy(mainConf.getAssetPath("themes") / "default_dark.conf", fncPath / "themes") - shutil.copy(mainConf.getAssetPath("themes") / "default.conf", fncPath / "themes") + shutil.copy(mainConf.assetPath("themes") / "default_dark.conf", fncPath / "themes") + shutil.copy(mainConf.assetPath("themes") / "default.conf", fncPath / "themes") # Block the reading of the files with monkeypatch.context() as mp: @@ -199,8 +199,8 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI, fncPath): # List Themes # =========== - shutil.copy(mainConf.getAssetPath("syntax") / "default_dark.conf", fncPath / "syntax") - shutil.copy(mainConf.getAssetPath("syntax") / "default_light.conf", fncPath / "syntax") + shutil.copy(mainConf.assetPath("syntax") / "default_dark.conf", fncPath / "syntax") + shutil.copy(mainConf.assetPath("syntax") / "default_light.conf", fncPath / "syntax") # Block the reading of the files with monkeypatch.context() as mp: From cb755ba820a394a2e50d41101aef3b9d3ee070de Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 9 Nov 2022 20:54:13 +0100 Subject: [PATCH 7/9] Move recent cache out of config, and update all tests --- novelwriter/config.py | 206 ++++++++------- novelwriter/core/project.py | 6 +- novelwriter/dialogs/projload.py | 26 +- novelwriter/guimain.py | 2 +- tests/test_base/test_base_config.py | 378 ++++++++++++++-------------- 5 files changed, 313 insertions(+), 305 deletions(-) diff --git a/novelwriter/config.py b/novelwriter/config.py index af523dc5..8c21dc99 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -74,8 +74,8 @@ class Config: self._appPath = self._appRoot # Runtime Settings and Variables - self.hasError = False # True if the config class encountered an error - self.errData = [] # List of error messages + self._hasError = False # True if the config class encountered an error + self._errData = [] # List of error messages self.confChanged = False # True whenever the config has chenged, false after save self.cmdOpen = None # Path from command line for project to be opened on launch @@ -92,6 +92,8 @@ class Config: # User Settings # ============= + self._recentProj = RecentProjects(self._dataPath) + # General GUI Settings self.guiLang = self._qLocal.name() self.guiTheme = "" # GUI theme @@ -245,6 +247,18 @@ class Config: return + ## + # Properties + ## + + @property + def hasError(self): + return self._hasError + + @property + def recentProjects(self): + return self._recentProj + ## # Methods ## @@ -282,6 +296,15 @@ class Config: return self._lastPath return Path.home().absolute() + def errorText(self): + """Compile and return error messages from the initialisation of + the Config class, and clear the error buffer. + """ + errMessage = "
".join(self._errData) + self._hasError = False + self._errData = [] + return errMessage + ## # Config Actions ## @@ -321,12 +344,12 @@ class Config: else: self.saveConfig() - self.loadRecentCache() + self._recentProj.loadCache() self._checkOptionalPackages() logger.debug("Config initialisation complete") - return True + return def initLocalisation(self, nwApp): """Initialise the localisation of the GUI. @@ -345,7 +368,7 @@ class Config: qTrans = QTranslator() lngFile = "%s_%s" % (lngBase, lngCode.replace("-", "_")) if lngFile not in self._qtTrans: - if qTrans.load(lngFile, lngPath): + if qTrans.load(lngFile, str(lngPath)): logger.debug("Loaded: %s", os.path.join(lngPath, lngFile)) nwApp.installTranslator(qTrans) self._qtTrans[lngFile] = qTrans @@ -367,12 +390,12 @@ class Config: else: return [] - for qmFile in os.listdir(self._nwLangPath): - if not os.path.isfile(os.path.join(self._nwLangPath, qmFile)): + for qmFile in Path(self._nwLangPath).iterdir(): + qmName = qmFile.name + if not (qmFile.is_file() and qmName.startswith(fPre) and qmName.endswith(fExt)): continue - if not qmFile.startswith(fPre) or not qmFile.endswith(fExt): - continue - qmLang = qmFile[len(fPre):-len(fExt)] + + qmLang = qmName[len(fPre):-len(fExt)] qmName = QLocale(qmLang).nativeLanguageName().title() if qmLang and qmName and qmLang != "en_GB": langList[qmLang] = qmName @@ -392,9 +415,9 @@ class Config: except Exception as exc: logger.error("Could not load config file") logException() - self.hasError = True - self.errData.append("Could not load config file") - self.errData.append(formatException(exc)) + self._hasError = True + self._errData.append("Could not load config file") + self._errData.append(formatException(exc)) return False # Main @@ -606,80 +629,13 @@ class Config: except Exception as exc: logger.error("Could not save config file") logException() - self.hasError = True - self.errData.append("Could not save config file") - self.errData.append(formatException(exc)) + self._hasError = True + self._errData.append("Could not save config file") + self._errData.append(formatException(exc)) return False return True - def loadRecentCache(self): - """Load the cache file for recent projects. - """ - self.recentProj = {} - - cacheFile = self._dataPath / nwFiles.RECENT_FILE - if not os.path.isfile(cacheFile): - return True - - try: - with open(cacheFile, mode="r", encoding="utf-8") as inFile: - theData = json.load(inFile) - - for projPath, theEntry in theData.items(): - self.recentProj[projPath] = { - "title": theEntry.get("title", ""), - "time": theEntry.get("time", 0), - "words": theEntry.get("words", 0), - } - - except Exception as exc: - self.hasError = True - self.errData.append("Could not load recent project cache") - self.errData.append(formatException(exc)) - return False - - return True - - def saveRecentCache(self): - """Save the cache dictionary of recent projects. - """ - cacheFile = self._dataPath / nwFiles.RECENT_FILE - cacheTemp = cacheFile.with_suffix(".tmp") - try: - with open(cacheTemp, mode="w+", encoding="utf-8") as outFile: - json.dump(self.recentProj, outFile, indent=2) - cacheTemp.replace(cacheFile) - except Exception as exc: - self.hasError = True - self.errData.append("Could not save recent project cache") - self.errData.append(formatException(exc)) - return False - - return True - - def updateRecentCache(self, projPath, projTitle, wordCount, saveTime): - """Add or update recent cache information on a given project. - """ - self.recentProj[os.path.abspath(projPath)] = { - "title": projTitle, - "time": int(saveTime), - "words": int(wordCount), - } - return True - - def removeFromRecentCache(self, thePath): - """Trying to remove a path from the recent projects cache. - """ - if thePath in self.recentProj: - del self.recentProj[thePath] - logger.debug("Removed recent: %s", thePath) - self.saveRecentCache() - else: - logger.error("Unknown recent: %s", thePath) - return False - return True - ## # Setters ## @@ -828,15 +784,6 @@ class Config: def getTabWidth(self): return self.pxInt(max(self.tabWidth, 0)) - def getErrData(self): - """Compile and return error messages from the initialisation of - the Config class, and clear the error buffer. - """ - errMessage = "
".join(self.errData) - self.hasError = False - self.errData = [] - return errMessage - ## # Internal Functions ## @@ -872,3 +819,78 @@ class Config: return # END Class Config + + +class RecentProjects: + + def __init__(self, dataPath): + self._dataPath = dataPath + self._data = {} + return + + def loadCache(self): + """Load the cache file for recent projects. + """ + self._data = {} + + cacheFile = self._dataPath / nwFiles.RECENT_FILE + if not cacheFile.is_file(): + return True + + try: + with open(cacheFile, mode="r", encoding="utf-8") as inFile: + theData = json.load(inFile) + for projPath, theEntry in theData.items(): + self._data[projPath] = { + "title": theEntry.get("title", ""), + "words": theEntry.get("words", 0), + "time": theEntry.get("time", 0), + } + except Exception: + logger.error("Could not load recent project cache") + logException() + return False + + return True + + def saveCache(self): + """Save the cache dictionary of recent projects. + """ + cacheFile = self._dataPath / nwFiles.RECENT_FILE + cacheTemp = cacheFile.with_suffix(".tmp") + try: + with open(cacheTemp, mode="w+", encoding="utf-8") as outFile: + json.dump(self._data, outFile, indent=2) + cacheTemp.replace(cacheFile) + except Exception: + logger.error("Could not save recent project cache") + logException() + return False + + return True + + def listEntries(self): + """List all items in the cache. + """ + return [(k, e["title"], e["words"], e["time"]) for k, e in self._data.items()] + + def update(self, projPath, projTitle, wordCount, saveTime): + """Add or update recent cache information on a given project. + """ + self._data[str(projPath)] = { + "title": projTitle, + "words": int(wordCount), + "time": int(saveTime), + } + self.saveCache() + return + + def remove(self, projPath): + """Try to remove a path from the recent projects cache. + """ + if self._data.pop(str(projPath), None) is not None: + logger.debug("Removed recent: %s", projPath) + self.saveCache() + return + +# END Class RecentProjects diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 49df331a..ae4f7bab 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -354,10 +354,9 @@ class NWProject(QObject): self._loadProjectLocalisation() # Update recent projects - self.mainConf.updateRecentCache( + self.mainConf.recentProjects.update( self._storage.storagePath, self._data.name, sum(self._data.initCounts), time() ) - self.mainConf.saveRecentCache() # Check the project tree consistency for tItem in self._tree: @@ -424,10 +423,9 @@ class NWProject(QObject): self._storage.runPostSaveTasks(autoSave=autoSave) # Update recent projects - self.mainConf.updateRecentCache( + self.mainConf.recentProjects.update( self._storage.storagePath, self._data.name, sum(self._data.currCounts), saveTime ) - self.mainConf.saveRecentCache() self._storage.writeLockFile() self.mainGui.setStatus(self.tr("Saved Project: {0}").format(self._data.name)) diff --git a/novelwriter/dialogs/projload.py b/novelwriter/dialogs/projload.py index c5568f62..ecd2f9f4 100644 --- a/novelwriter/dialogs/projload.py +++ b/novelwriter/dialogs/projload.py @@ -229,7 +229,7 @@ class GuiProjectLoad(QDialog): ).format(projName) ) if msgYes: - self.mainConf.removeFromRecentCache( + self.mainConf.recentProjects.remove( selList[0].data(self.C_NAME, Qt.UserRole) ) self._populateList() @@ -264,23 +264,17 @@ class GuiProjectLoad(QDialog): def _populateList(self): """Populate the list box with recent project data. """ - dataList = [] - for projPath in self.mainConf.recentProj: - theEntry = self.mainConf.recentProj[projPath] - theTitle = theEntry.get("title", "") - theTime = theEntry.get("time", 0) - theWords = theEntry.get("words", 0) - dataList.append([theTitle, theTime, theWords, projPath]) - self.listBox.clear() - sortList = sorted(dataList, key=lambda x: x[1], reverse=True) - for theTitle, theTime, theWords, projPath in sortList: + dataList = self.mainConf.recentProjects.listEntries() + sortList = sorted(dataList, key=lambda x: x[3], reverse=True) + nwxIcon = self.mainGui.mainTheme.getIcon("proj_nwx") + for path, title, words, time in sortList: newItem = QTreeWidgetItem([""]*4) - newItem.setIcon(self.C_NAME, self.mainGui.mainTheme.getIcon("proj_nwx")) - newItem.setText(self.C_NAME, theTitle) - newItem.setData(self.C_NAME, Qt.UserRole, projPath) - newItem.setText(self.C_COUNT, formatInt(theWords)) - newItem.setText(self.C_TIME, datetime.fromtimestamp(theTime).strftime("%x %X")) + newItem.setIcon(self.C_NAME, nwxIcon) + newItem.setText(self.C_NAME, title) + newItem.setData(self.C_NAME, Qt.UserRole, path) + newItem.setText(self.C_COUNT, formatInt(words)) + newItem.setText(self.C_TIME, datetime.fromtimestamp(time).strftime("%x %X")) newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft | Qt.AlignVCenter) newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter) newItem.setTextAlignment(self.C_TIME, Qt.AlignRight | Qt.AlignVCenter) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index fb77ca8a..09df46fb 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -1144,7 +1144,7 @@ class GuiMain(QMainWindow): errors since it is initialised before the GUI itself. """ if self.mainConf.hasError: - self.makeAlert(self.mainConf.getErrData(), nwAlert.ERROR) + self.makeAlert(self.mainConf.errorText(), nwAlert.ERROR) return True return False diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py index fdca0b8b..bac296ce 100644 --- a/tests/test_base/test_base_config.py +++ b/tests/test_base/test_base_config.py @@ -19,16 +19,16 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import sys import pytest from shutil import copyfile +from pathlib import Path from mock import causeOSError, MockApp from tools import cmpFiles, writeFile -from novelwriter.config import Config +from novelwriter.config import Config, RecentProjects from novelwriter.constants import nwFiles @@ -37,173 +37,140 @@ def testBaseConfig_Constructor(monkeypatch): """Test config contructor. """ # Linux - monkeypatch.setattr("sys.platform", "linux") - tstConf = Config() - assert tstConf.osLinux is True - assert tstConf.osDarwin is False - assert tstConf.osWindows is False - assert tstConf.osUnknown is False + with monkeypatch.context() as mp: + mp.setattr("sys.platform", "linux") + tstConf = Config() + assert tstConf.osLinux is True + assert tstConf.osDarwin is False + assert tstConf.osWindows is False + assert tstConf.osUnknown is False # macOS - monkeypatch.setattr("sys.platform", "darwin") - tstConf = Config() - assert tstConf.osLinux is False - assert tstConf.osDarwin is True - assert tstConf.osWindows is False - assert tstConf.osUnknown is False + with monkeypatch.context() as mp: + mp.setattr("sys.platform", "darwin") + tstConf = Config() + assert tstConf.osLinux is False + assert tstConf.osDarwin is True + assert tstConf.osWindows is False + assert tstConf.osUnknown is False # Windows - monkeypatch.setattr("sys.platform", "win32") - tstConf = Config() - assert tstConf.osLinux is False - assert tstConf.osDarwin is False - assert tstConf.osWindows is True - assert tstConf.osUnknown is False + with monkeypatch.context() as mp: + mp.setattr("sys.platform", "win32") + tstConf = Config() + assert tstConf.osLinux is False + assert tstConf.osDarwin is False + assert tstConf.osWindows is True + assert tstConf.osUnknown is False # Cygwin - monkeypatch.setattr("sys.platform", "cygwin") - tstConf = Config() - assert tstConf.osLinux is False - assert tstConf.osDarwin is False - assert tstConf.osWindows is True - assert tstConf.osUnknown is False + with monkeypatch.context() as mp: + mp.setattr("sys.platform", "cygwin") + tstConf = Config() + assert tstConf.osLinux is False + assert tstConf.osDarwin is False + assert tstConf.osWindows is True + assert tstConf.osUnknown is False # Other - monkeypatch.setattr("sys.platform", "some_other_os") - tstConf = Config() - assert tstConf.osLinux is False - assert tstConf.osDarwin is False - assert tstConf.osWindows is False - assert tstConf.osUnknown is True + with monkeypatch.context() as mp: + mp.setattr("sys.platform", "some_other_os") + tstConf = Config() + assert tstConf.osLinux is False + assert tstConf.osDarwin is False + assert tstConf.osWindows is False + assert tstConf.osUnknown is True + + # App is single file + with monkeypatch.context() as mp: + mp.setattr("pathlib.Path.is_file", lambda *a: True) + tstConf = Config() + assert tstConf._appPath == tstConf._appRoot # END Test testBaseConfig_Constructor @pytest.mark.base -@pytest.mark.skip -def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): +def testBaseConfig_InitLoadSave(monkeypatch, fncPath, tstPaths): """Test config intialisation. """ tstConf = Config() - confFile = os.path.join(tmpDir, "novelwriter.conf") - testFile = os.path.join(outDir, "baseConfig_novelwriter.conf") - compFile = os.path.join(refDir, "baseConfig_novelwriter.conf") + confFile = fncPath / nwFiles.CONF_FILE + testFile = tstPaths.outDir / "baseConfig_novelwriter.conf" + compFile = tstPaths.refDir / "baseConfig_novelwriter.conf" # Make sure we don't have any old conf file - if os.path.isfile(confFile): - os.unlink(confFile) + if confFile.is_file(): + confFile.unlink() - # Let the config class figure out the path - with monkeypatch.context() as mp: - mp.setattr("PyQt5.QtCore.QStandardPaths.writableLocation", lambda *a: fncDir) - tstConf.initConfig() - assert tstConf._confPath == os.path.join(fncDir, tstConf.appHandle) - assert tstConf._dataPath == os.path.join(fncDir, tstConf.appHandle) - assert not os.path.isfile(confFile) + # Running init against a new oath should write a new config file + tstConf.initConfig(confPath=fncPath, dataPath=fncPath) + assert tstConf._confPath == fncPath + assert tstConf._dataPath == fncPath + assert confFile.exists() - # Fail to make folders - with monkeypatch.context() as mp: - mp.setattr("os.mkdir", causeOSError) + # Check that we have a default file + copyfile(confFile, testFile) + ignore = ("timestamp", "lastnotes", "guilang", "lastpath") + assert cmpFiles(testFile, compFile, ignoreStart=ignore) + tstConf.errorText() # This clears the error cache - tstConfDir = os.path.join(fncDir, "test_conf") - tstConf.initConfig(confPath=tstConfDir, dataPath=tmpDir) - assert tstConf._confPath is None - assert tstConf._dataPath == tmpDir - assert not os.path.isfile(confFile) - - tstDataDir = os.path.join(fncDir, "test_data") - tstConf.initConfig(confPath=tmpDir, dataPath=tstDataDir) - assert tstConf._confPath == tmpDir - assert tstConf._dataPath is None - assert os.path.isfile(confFile) - os.unlink(confFile) - - # Test load/save with no path - tstConf._confPath = None - assert tstConf.loadConfig() is False - assert tstConf.saveConfig() is False - - # Run again and set the paths directly and correctly - # This should create a config file as well - with monkeypatch.context() as mp: - mp.setattr("os.path.expanduser", lambda *a: "") - tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir) - assert tstConf._confPath == tmpDir - assert tstConf._dataPath == tmpDir - assert os.path.isfile(confFile) - - copyfile(confFile, testFile) - assert cmpFiles(testFile, compFile, ignoreStart=("timestamp", "lastnotes", "guilang")) - - # Load and save with OSError + # Block saving the file with monkeypatch.context() as mp: mp.setattr("builtins.open", causeOSError) - - assert not tstConf.loadConfig() + assert tstConf.saveConfig() is False assert tstConf.hasError is True - assert tstConf.errData != [] - assert tstConf.getErrData().startswith("Could not") - assert tstConf.hasError is False - assert tstConf.errData == [] + assert tstConf.errorText().startswith("Could not save config file") - assert not tstConf.saveConfig() - assert tstConf.hasError is True - assert tstConf.errData != [] - assert tstConf.getErrData().startswith("Could not") - assert tstConf.hasError is False - assert tstConf.errData == [] - - # Check handling of novelWriter as a package + # Block loading the file with monkeypatch.context() as mp: - tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir) - assert tstConf._confPath == tmpDir - assert tstConf._dataPath == tmpDir - appRoot = tstConf._appRoot + mp.setattr("builtins.open", causeOSError) + assert tstConf.loadConfig() is False + assert tstConf.hasError is True + assert tstConf.errorText().startswith("Could not load config file") - mp.setattr("os.path.isfile", lambda *a: True) - tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir) - assert tstConf._confPath == tmpDir - assert tstConf._dataPath == tmpDir - assert tstConf._appRoot == os.path.dirname(appRoot) - assert tstConf._appPath == os.path.dirname(appRoot) - - assert tstConf.loadConfig() is True + # Change a few settings, save, reset, and reload + tstConf.guiTheme = "foo" + tstConf.guiSyntax = "bar" assert tstConf.saveConfig() is True - # Test Correcting Quote Settings - origDbl = tstConf.fmtDoubleQuotes - origSng = tstConf.fmtSingleQuotes - orDoDbl = tstConf.doReplaceDQuote - orDoSng = tstConf.doReplaceSQuote + newConf = Config() + newConf.initConfig(confPath=fncPath, dataPath=fncPath) + assert newConf.guiTheme == "foo" + assert newConf.guiSyntax == "bar" + # Test Correcting Quote Settings tstConf.fmtDoubleQuotes = ["\"", "\""] tstConf.fmtSingleQuotes = ["'", "'"] tstConf.doReplaceDQuote = True tstConf.doReplaceSQuote = True assert tstConf.saveConfig() is True - assert tstConf.loadConfig() is True - assert tstConf.doReplaceDQuote is False - assert tstConf.doReplaceSQuote is False + assert newConf.loadConfig() is True + assert newConf.doReplaceDQuote is False + assert newConf.doReplaceSQuote is False - tstConf.fmtDoubleQuotes = origDbl - tstConf.fmtSingleQuotes = origSng - tstConf.doReplaceDQuote = orDoDbl - tstConf.doReplaceSQuote = orDoSng - assert tstConf.saveConfig() is True +# END Test testBaseConfig_InitLoadSave + + +@pytest.mark.base +def testBaseConfig_Localisation(fncPath, tstPaths): + """Test localisation. + """ + tstConf = Config() + tstConf.initConfig(confPath=fncPath, dataPath=fncPath) # Localisation # ============ - i18nDir = os.path.join(fncDir, "i18n") - os.mkdir(i18nDir) - os.mkdir(os.path.join(i18nDir, "stuff")) + i18nDir = fncPath / "i18n" + i18nDir.mkdir() tstConf._nwLangPath = i18nDir - copyfile(os.path.join(filesDir, "nw_en_GB.qm"), os.path.join(i18nDir, "nw_en_GB.qm")) - writeFile(os.path.join(i18nDir, "nw_en_GB.ts"), "") - writeFile(os.path.join(i18nDir, "nw_abcd.qm"), "") + copyfile(tstPaths.filesDir / "nw_en_GB.qm", i18nDir / "nw_en_GB.qm") + writeFile(i18nDir / "nw_en_GB.ts", "") + writeFile(i18nDir / "nw_abcd.qm", "") tstApp = MockApp() tstConf.initLocalisation(tstApp) @@ -217,82 +184,55 @@ def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): assert theList == [] # Add Language - copyfile(os.path.join(filesDir, "nw_en_GB.qm"), os.path.join(i18nDir, "nw_fr.qm")) - writeFile(os.path.join(i18nDir, "nw_fr.ts"), "") + copyfile(tstPaths.filesDir / "nw_en_GB.qm", i18nDir / "nw_fr.qm") + writeFile(i18nDir / "nw_fr.ts", "") theList = tstConf.listLanguages(tstConf.LANG_NW) assert theList == [("en_GB", "British English"), ("fr", "Français")] - copyfile(confFile, testFile) - assert cmpFiles(testFile, compFile, ignoreStart=("timestamp", "lastnotes", "guilang")) - -# END Test testBaseConfig_Init +# END Test testBaseConfig_Localisation @pytest.mark.base -def testBaseConfig_RecentCache(monkeypatch, tmpConf, tmpDir, fncDir): - """Test recent cache file. +def testBaseConfig_Methods(tmpConf, tmpPath): + """Check class methods. """ - # Add a couple of values - pathOne = os.path.join(fncDir, "projPathOne", nwFiles.PROJ_FILE) - pathTwo = os.path.join(fncDir, "projPathTwo", nwFiles.PROJ_FILE) - assert tmpConf.updateRecentCache(pathOne, "Proj One", 100, 1600002000) - assert tmpConf.updateRecentCache(pathTwo, "Proj Two", 200, 1600005600) - assert tmpConf.recentProj == { - pathOne: {"time": 1600002000, "title": "Proj One", "words": 100}, - pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200}, - } + # Data Path + assert tmpConf.dataPath() == tmpPath + assert tmpConf.dataPath("stuff") == tmpPath / "stuff" - # Fail to Save - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - assert not tmpConf.saveRecentCache() + # Assets Path + appPath = tmpConf._appPath + assert tmpConf.assetPath() == appPath / "assets" + assert tmpConf.assetPath("stuff") == appPath / "assets" / "stuff" - # Save Proper - cacheFile = os.path.join(tmpDir, nwFiles.RECENT_FILE) - assert tmpConf.saveRecentCache() - assert tmpConf.saveRecentCache() - assert os.path.isfile(cacheFile) + # Last Path + assert tmpConf.lastPath() == tmpPath - # Fail to Load - with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) - tmpConf.recentProj = {} - assert not tmpConf.loadRecentCache() - assert tmpConf.recentProj == {} + tmpStuff = tmpPath / "stuff" + tmpStuff.mkdir() + tmpConf.setLastPath(tmpStuff) + assert tmpConf.lastPath() == tmpStuff - # Load Proper - tmpConf.recentProj = {} - assert tmpConf.loadRecentCache() - assert tmpConf.recentProj == { - pathOne: {"time": 1600002000, "title": "Proj One", "words": 100}, - pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200}, - } + fileStuff = tmpStuff / "more_stuff.txt" + fileStuff.write_text("Stuff") + tmpConf.setLastPath(fileStuff) + assert tmpConf.lastPath() == tmpStuff - # Remove Non-Existent Entry - assert not tmpConf.removeFromRecentCache("stuff") - assert tmpConf.recentProj == { - pathOne: {"time": 1600002000, "title": "Proj One", "words": 100}, - pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200}, - } + fileStuff.unlink() + tmpStuff.rmdir() + assert tmpConf.lastPath() == Path.home().absolute() - # Remove Second Entry - assert tmpConf.removeFromRecentCache(pathTwo) - assert tmpConf.recentProj == { - pathOne: {"time": 1600002000, "title": "Proj One", "words": 100}, - } + # Recent Projects + assert isinstance(tmpConf.recentProjects, RecentProjects) -# END Test testBaseConfig_RecentCache +# END Test testBaseConfig_Methods @pytest.mark.base -def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir): +def testBaseConfig_SettersGetters(tmpConf): """Set various sizes and positions """ - confFile = os.path.join(tmpDir, "novelwriter.conf") - testFile = os.path.join(outDir, "baseConfig_novelwriter.conf") - compFile = os.path.join(refDir, "baseConfig_novelwriter.conf") - # GUI Scaling # =========== @@ -439,17 +379,6 @@ def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir): tmpConf.setViewSynopsis(True) assert tmpConf.viewSynopsis is True - # Check Final File - # ================ - - assert tmpConf.confChanged is True - assert tmpConf.saveConfig() is True - assert tmpConf.confChanged is False - - copyfile(confFile, testFile) - ignore = ("timestamp", "lastnotes", "guilang", "lastpath") - assert cmpFiles(testFile, compFile, ignoreStart=ignore) - # END Test testBaseConfig_SettersGetters @@ -479,3 +408,68 @@ def testBaseConfig_Internal(monkeypatch, tmpConf): assert tmpConf.hasEnchant is False # END Test testBaseConfig_Internal + + +@pytest.mark.base +def testBaseConfig_RecentCache(monkeypatch, fncPath): + """Test recent cache file. + """ + cacheFile = fncPath / nwFiles.RECENT_FILE + recent = RecentProjects(fncPath) + + # Load when there is no file should pass, but load nothing + assert not cacheFile.exists() + assert recent.loadCache() is True + assert recent.listEntries() == [] + + # Add a couple of values + pathOne = fncPath / "projPathOne" / nwFiles.PROJ_FILE + pathTwo = fncPath / "projPathTwo" / nwFiles.PROJ_FILE + + recent.update(pathOne, "Proj One", 100, 1600002000) + recent.update(pathTwo, "Proj Two", 200, 1600005600) + assert recent.listEntries() == [ + (str(pathOne), "Proj One", 100, 1600002000), + (str(pathTwo), "Proj Two", 200, 1600005600), + ] + assert cacheFile.exists() + cacheFile.unlink() + assert not cacheFile.exists() + + # Fail to Save + with monkeypatch.context() as mp: + mp.setattr("builtins.open", causeOSError) + assert recent.saveCache() is False + assert not cacheFile.exists() + + # Save Proper + assert recent.saveCache() is True + assert cacheFile.exists() + + # Fail to Load + with monkeypatch.context() as mp: + mp.setattr("builtins.open", causeOSError) + assert recent.loadCache() is False + assert recent.listEntries() == [] + + # Load Proper + assert recent.loadCache() is True + assert recent.listEntries() == [ + (str(pathOne), "Proj One", 100, 1600002000), + (str(pathTwo), "Proj Two", 200, 1600005600), + ] + + # Remove Non-Existent Entry + recent.remove("stuff") + assert recent.listEntries() == [ + (str(pathOne), "Proj One", 100, 1600002000), + (str(pathTwo), "Proj Two", 200, 1600005600), + ] + + # Remove Second Entry + recent.remove(pathTwo) + assert recent.listEntries() == [ + (str(pathOne), "Proj One", 100, 1600002000), + ] + +# END Test testBaseConfig_RecentCache From 03e173634f27db85371b3672a183871c5e7ba76b Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 9 Nov 2022 22:43:48 +0100 Subject: [PATCH 8/9] Switch to Path objects nearly everywhere --- novelwriter/common.py | 33 +----- novelwriter/config.py | 26 ++++- novelwriter/core/project.py | 5 +- novelwriter/dialogs/preferences.py | 12 +-- novelwriter/dialogs/projload.py | 6 +- novelwriter/gui/mainmenu.py | 2 +- novelwriter/guimain.py | 5 +- tests/conftest.py | 108 +++++++------------ tests/test_base/test_base_common.py | 53 +++------ tests/test_base/test_base_error.py | 24 +---- tests/test_base/test_base_init.py | 32 +++--- tests/test_core/test_core_coretools.py | 66 ++++++------ tests/test_core/test_core_index.py | 23 ++-- tests/test_core/test_core_item.py | 8 +- tests/test_core/test_core_project.py | 26 ++--- tests/test_core/test_core_tohtml.py | 5 +- tests/test_core/test_core_tokenizer.py | 7 +- tests/test_core/test_core_tomd.py | 5 +- tests/test_core/test_core_toodt.py | 51 +++++---- tests/test_dialogs/test_dlg_docmerge.py | 4 +- tests/test_dialogs/test_dlg_docsplit.py | 4 +- tests/test_dialogs/test_dlg_projload.py | 9 +- tests/test_dialogs/test_dlg_projsettings.py | 18 ++-- tests/test_dialogs/test_dlg_wordlist.py | 9 +- tests/test_gui/test_gui_doceditor.py | 40 +++---- tests/test_gui/test_gui_guimain.py | 71 ++++++------ tests/test_gui/test_gui_mainmenu.py | 13 ++- tests/test_gui/test_gui_noveltree.py | 4 +- tests/test_gui/test_gui_outline.py | 5 +- tests/test_gui/test_gui_projtree.py | 57 ++++------ tests/test_gui/test_gui_statusbar.py | 4 +- tests/test_tools/test_tools_build.py | 114 ++++++++++---------- tests/test_tools/test_tools_lipsum.py | 4 +- tests/test_tools/test_tools_projwizard.py | 21 ++-- tests/test_tools/test_tools_writingstats.py | 27 +++-- tests/tools.py | 26 ++--- 36 files changed, 406 insertions(+), 521 deletions(-) diff --git a/novelwriter/common.py b/novelwriter/common.py index 7eb1ec59..8b4c2da8 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -23,12 +23,12 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import json import uuid import hashlib import logging +from pathlib import Path from datetime import datetime from configparser import ConfigParser @@ -36,7 +36,7 @@ from PyQt5.QtCore import QCoreApplication from PyQt5.QtWidgets import qApp from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout -from novelwriter.error import formatException, logException +from novelwriter.error import logException from novelwriter.constants import nwConst, nwUnicode logger = logging.getLogger(__name__) @@ -458,20 +458,16 @@ def jsonEncode(data, n=0, nmax=0): def readTextFile(path): """Read the content of a text file in a robust manner. """ - if not os.path.isfile(path): + path = Path(path) + if not path.is_file(): return "" - - text = "" try: - with open(path, mode="r", encoding="utf-8") as inFile: - text = inFile.read() + return path.read_text(encoding="utf-8") except Exception: logger.error("Could not read file: %s", path) logException() return "" - return text - def makeFileNameSafe(value): """Returns a filename safe string of the value. @@ -483,25 +479,6 @@ def makeFileNameSafe(value): return clean -def ensureFolder(path, parent=None, errLog=None): - """Make sure a folder exists, and if it doesn't, create it. - """ - try: - if parent: - path = os.path.join(parent, path) - if not os.path.isdir(path): - os.mkdir(path) - except Exception as exc: - logger.error("Could not create folder: %s", path) - logException() - if isinstance(errLog, list): - errLog.append(f"Could not create folder: {path}") - errLog.append(formatException(exc)) - return False - - return True - - def sha256sum(path): """Make a shasum of a file using a buffer. Based on: https://stackoverflow.com/a/44873382/5825851 diff --git a/novelwriter/config.py b/novelwriter/config.py index 8c21dc99..454e54d1 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -23,7 +23,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import sys import json import logging @@ -184,7 +183,7 @@ class Config: self.searchMatchCap = False # Backup Settings - self.backupPath = "" + self._backupPath = None self.backupOnClose = False self.askBeforeBackup = True @@ -296,6 +295,14 @@ class Config: return self._lastPath return Path.home().absolute() + def backupPath(self): + """Return the backup path. + """ + if isinstance(self._backupPath, Path): + if self._backupPath.is_dir(): + return self._backupPath + return None + def errorText(self): """Compile and return error messages from the initialisation of the Config class, and clear the error buffer. @@ -369,7 +376,7 @@ class Config: lngFile = "%s_%s" % (lngBase, lngCode.replace("-", "_")) if lngFile not in self._qtTrans: if qTrans.load(lngFile, str(lngPath)): - logger.debug("Loaded: %s", os.path.join(lngPath, lngFile)) + logger.debug("Loaded: %s/%s", lngPath, lngFile) nwApp.installTranslator(qTrans) self._qtTrans[lngFile] = qTrans @@ -489,9 +496,10 @@ class Config: # Backup cnfSec = "Backup" - self.backupPath = theConf.rdStr(cnfSec, "backuppath", self.backupPath) + backupPath = theConf.rdStr(cnfSec, "backuppath", None) self.backupOnClose = theConf.rdBool(cnfSec, "backuponclose", self.backupOnClose) self.askBeforeBackup = theConf.rdBool(cnfSec, "askbeforebackup", self.askBeforeBackup) + self.setBackupPath(backupPath) # State cnfSec = "State" @@ -599,7 +607,7 @@ class Config: } theConf["Backup"] = { - "backuppath": str(self.backupPath), + "backuppath": str(self._backupPath or ""), "backuponclose": str(self.backupOnClose), "askbeforebackup": str(self.askBeforeBackup), } @@ -653,6 +661,14 @@ class Config: logger.debug("Last path updated: %s" % self._lastPath) return + def setBackupPath(self, backupPath): + """Set the current backup path. + """ + self._backupPath = None + if isinstance(backupPath, (str, Path)): + self._backupPath = Path(backupPath) + return + def setWinSize(self, newWidth, newHeight): """Set the size of the main window, but only if the change is larger than 5 pixels. The OS window manager will sometimes diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index ae4f7bab..7f01f632 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -456,7 +456,8 @@ class NWProject(QObject): logger.info("Backing up project") self.mainGui.setStatus(self.tr("Backing up project ...")) - if not self.mainConf.backupPath: + backupPath = self.mainConf.backupPath() + if not isinstance(backupPath, Path): self.mainGui.makeAlert(self.tr( "Cannot backup project because no valid backup path is set. " "Please set a valid backup location in Preferences." @@ -471,7 +472,7 @@ class NWProject(QObject): return False cleanName = makeFileNameSafe(self._data.name) - baseDir = Path(self.mainConf.backupPath) / cleanName + baseDir = backupPath / cleanName try: baseDir.mkdir(exist_ok=True) except Exception as exc: diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 72498421..81e62649 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -23,7 +23,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import logging import novelwriter @@ -384,7 +383,7 @@ class GuiPreferencesProjects(QWidget): self.mainForm.addGroupLabel(self.tr("Project Backup")) # Backup Path - self.backupPath = self.mainConf.backupPath + self.backupPath = self.mainConf.backupPath() self.backupGetPath = QPushButton(self.tr("Browse")) self.backupGetPath.clicked.connect(self._backupFolder) self.backupPathRow = self.mainForm.addRow( @@ -451,7 +450,7 @@ class GuiPreferencesProjects(QWidget): self.mainConf.autoSaveProj = self.autoSaveProj.value() # Project Backup - self.mainConf.backupPath = self.backupPath + self.mainConf.setBackupPath(self.backupPath) self.mainConf.backupOnClose = self.backupOnClose.isChecked() self.mainConf.askBeforeBackup = self.askBeforeBackup.isChecked() @@ -470,12 +469,9 @@ class GuiPreferencesProjects(QWidget): def _backupFolder(self): """Open a dialog to select the backup folder. """ - currDir = self.backupPath - if not os.path.isdir(currDir): - currDir = "" - + currDir = self.backupPath or "" newDir = QFileDialog.getExistingDirectory( - self, self.tr("Backup Directory"), currDir, options=QFileDialog.ShowDirsOnly + self, self.tr("Backup Directory"), str(currDir), options=QFileDialog.ShowDirsOnly ) if newDir: self.backupPath = newDir diff --git a/novelwriter/dialogs/projload.py b/novelwriter/dialogs/projload.py index ecd2f9f4..e646a0b0 100644 --- a/novelwriter/dialogs/projload.py +++ b/novelwriter/dialogs/projload.py @@ -23,10 +23,10 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import logging import novelwriter +from pathlib import Path from datetime import datetime from PyQt5.QtGui import QKeySequence @@ -190,8 +190,8 @@ class GuiProjectLoad(QDialog): self, self.tr("Open Project"), "", filter=";;".join(extFilter) ) if projFile: - thePath = os.path.abspath(os.path.dirname(projFile)) - self.selPath.setText(thePath) + thePath = Path(projFile).absolute() + self.selPath.setText(str(thePath)) self.openPath = thePath self.openState = self.OPEN_STATE self.accept() diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index b4acd6f2..77eff2b1 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -826,7 +826,7 @@ class GuiMainMenu(QMenuBar): # Tools > Backup self.aBackupProject = QAction(self.tr("Backup Project"), self) - self.aBackupProject.triggered.connect(lambda: self.theProject.backupProject(doNoify=True)) + self.aBackupProject.triggered.connect(lambda: self.theProject.backupProject(True)) self.toolsMenu.addAction(self.aBackupProject) # Tools > Export Project diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 09df46fb..4acb2952 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -23,7 +23,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import logging import novelwriter @@ -362,7 +361,7 @@ class GuiMain(QMainWindow): logger.error("No projData or projPath set") return False - if os.path.isfile(os.path.join(projPath, nwFiles.PROJ_FILE)): + if (Path(projPath) / nwFiles.PROJ_FILE).is_file(): self.makeAlert(self.tr( "A project already exists in that location. " "Please choose another folder." @@ -414,7 +413,7 @@ class GuiMain(QMainWindow): if not msgYes: doBackup = False if doBackup: - self.theProject.backupProject(doNotify=False) + self.theProject.backupProject(False) else: saveOK = True diff --git a/tests/conftest.py b/tests/conftest.py index 696d416e..28034a18 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import sys import pytest import shutil @@ -50,25 +49,14 @@ def initQt(qtbot): ## @pytest.fixture(scope="session") -def tmpDir(): - """A temporary folder for the test session. This folder is - presistent after the test so that the status of generated files can - be checked. The folder is instead cleared before a new test session. - """ - testDir = os.path.dirname(__file__) - theDir = os.path.join(testDir, "temp") - if os.path.isdir(theDir): - shutil.rmtree(theDir) - if not os.path.isdir(theDir): - os.mkdir(theDir) - return theDir - - -@pytest.fixture(scope="session") -def tmpPath(tmpDir): +def tmpPath(): """A temporary folder for the test session. Path version. """ - return Path(tmpDir) + theTemp = Path(__file__).parent / "temp" + if theTemp.exists(): + shutil.rmtree(theTemp) + theTemp.mkdir(exist_ok=True) + return theTemp @pytest.fixture(scope="session") @@ -99,57 +87,15 @@ def fncPath(tmpPath): return fncPath -@pytest.fixture(scope="session") -def refDir(): - """The folder where all the reference files are stored for verifying - the results of tests. - """ - testDir = os.path.dirname(__file__) - theDir = os.path.join(testDir, "reference") - return theDir - - -@pytest.fixture(scope="session") -def filesDir(): - """The folder where additional test files are stored. - """ - testDir = os.path.dirname(__file__) - theDir = os.path.join(testDir, "files") - return theDir - - -@pytest.fixture(scope="session") -def outDir(tmpDir): - """An output folder for test results - """ - theDir = os.path.join(tmpDir, "results") - if not os.path.isdir(theDir): - os.mkdir(theDir) - return theDir - - @pytest.fixture(scope="function") -def fncDir(tmpDir): - """A temporary folder for a single test function. - """ - fncDir = os.path.join(tmpDir, "function") - if os.path.isdir(fncDir): - shutil.rmtree(fncDir) - if not os.path.isdir(fncDir): - os.mkdir(fncDir) - return fncDir - - -@pytest.fixture(scope="function") -def fncProj(fncDir): +def projPath(fncPath): """A temporary folder for a single test function, with a project folder. """ - prjDir = os.path.join(fncDir, "project") - if os.path.isdir(prjDir): + prjDir = fncPath / "project" + if prjDir.exists(): shutil.rmtree(prjDir) - if not os.path.isdir(prjDir): - os.mkdir(prjDir) + prjDir.mkdir(exist_ok=True) return prjDir @@ -252,14 +198,36 @@ def mockRnd(monkeypatch): ## @pytest.fixture(scope="function") -def nwLipsum(tmpDir): +def nwLipsum(tmpPath): """A medium sized novelWriter example project with a lot of Lorem Ipsum text. """ - tstDir = os.path.dirname(__file__) - srcDir = os.path.join(tstDir, "lipsum") - dstDir = os.path.join(tmpDir, "lipsum") - if os.path.isdir(dstDir): + tstDir = Path(__file__).parent + srcDir = tstDir / "lipsum" + dstDir = tmpPath / "lipsum" + if dstDir.exists(): + shutil.rmtree(dstDir) + + shutil.copytree(srcDir, dstDir) + cleanProject(dstDir) + + yield str(dstDir) + + if dstDir.exists(): + shutil.rmtree(dstDir) + + return + + +@pytest.fixture(scope="function") +def prjLipsum(tmpPath): + """A medium sized novelWriter example project with a lot of Lorem + Ipsum text. + """ + tstDir = Path(__file__).parent + srcDir = tstDir / "lipsum" + dstDir = tmpPath / "lipsum" + if dstDir.exists(): shutil.rmtree(dstDir) shutil.copytree(srcDir, dstDir) @@ -267,7 +235,7 @@ def nwLipsum(tmpDir): yield dstDir - if os.path.isdir(dstDir): + if dstDir.exists(): shutil.rmtree(dstDir) return diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index f6d60a1c..adabbfe7 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -19,10 +19,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import hashlib -import os import time import pytest +import hashlib from mock import causeOSError from tools import writeFile @@ -33,8 +32,8 @@ from novelwriter.common import ( checkUuid, isHandle, isTitleTag, isItemClass, isItemType, isItemLayout, hexToInt, minmax, checkIntTuple, formatInt, formatTimeStamp, formatTime, simplified, yesNo, splitVersionNumber, transferCase, fuzzyTime, - numberToRoman, jsonEncode, readTextFile, makeFileNameSafe, ensureFolder, - sha256sum, getGuiItem, NWConfigParser + numberToRoman, jsonEncode, readTextFile, makeFileNameSafe, sha256sum, + getGuiItem, NWConfigParser ) @@ -591,18 +590,18 @@ def testBaseCommon_JsonEncode(): @pytest.mark.base -def testBaseCommon_ReadTextFile(monkeypatch, fncDir, ipsumText): +def testBaseCommon_ReadTextFile(monkeypatch, fncPath, ipsumText): """Test the readTextFile function. """ testText = "\n\n".join(ipsumText) + "\n" - testFile = os.path.join(fncDir, "ipsum.txt") + testFile = fncPath / "ipsum.txt" writeFile(testFile, testText) - assert readTextFile(os.path.join(fncDir, "not_a_file.txt")) == "" + assert readTextFile(fncPath / "not_a_file.txt") == "" assert readTextFile(testFile) == testText with monkeypatch.context() as mp: - mp.setattr("builtins.open", causeOSError) + mp.setattr("pathlib.Path.read_text", causeOSError) assert readTextFile(testFile) == "" # END Test testBaseCommon_ReadTextFile @@ -621,33 +620,7 @@ def testBaseCommon_MakeFileNameSafe(): @pytest.mark.base -def testBaseCommon_EnsureFolder(monkeypatch, fncDir): - """Test the ensureFolder function. - """ - newDir1 = os.path.join(fncDir, "newDir1") - newDir2 = os.path.join(fncDir, "newDir2") - newDir3 = os.path.join(fncDir, "newDir3") - - assert ensureFolder(None) is False - - assert ensureFolder(newDir1) is True - assert os.path.isdir(newDir1) - - assert ensureFolder("newDir2", parent=fncDir) is True - assert os.path.isdir(newDir2) - - with monkeypatch.context() as mp: - mp.setattr("os.mkdir", causeOSError) - errLog = [] - assert ensureFolder("newDir3", parent=fncDir, errLog=errLog) is False - assert errLog[0] == f"Could not create folder: {newDir3}" - assert not os.path.isdir(newDir3) - -# END Test testBaseCommon_EnsureFolder - - -@pytest.mark.base -def testBaseCommon_Sha256Sum(monkeypatch, fncDir, ipsumText): +def testBaseCommon_Sha256Sum(monkeypatch, fncPath, ipsumText): """Test the sha256sum function. """ longText = 50*(" ".join(ipsumText) + " ") @@ -656,9 +629,9 @@ def testBaseCommon_Sha256Sum(monkeypatch, fncDir, ipsumText): assert len(longText) == 175650 - longFile = os.path.join(fncDir, "long_file.txt") - shortFile = os.path.join(fncDir, "short_file.txt") - noneFile = os.path.join(fncDir, "none_file.txt") + longFile = fncPath / "long_file.txt" + shortFile = fncPath / "short_file.txt" + noneFile = fncPath / "none_file.txt" writeFile(longFile, longText) writeFile(shortFile, shortText) @@ -697,10 +670,10 @@ def testBaseCommon_GetGuiItem(nwGUI): @pytest.mark.base -def testBaseCommon_NWConfigParser(fncDir): +def testBaseCommon_NWConfigParser(fncPath): """Test the NWConfigParser subclass. """ - tstConf = os.path.join(fncDir, "test.cfg") + tstConf = fncPath / "test.cfg" writeFile(tstConf, ( "[main]\n" "stropt = value\n" diff --git a/tests/test_base/test_base_error.py b/tests/test_base/test_base_error.py index d2707b29..9ba9b9b2 100644 --- a/tests/test_base/test_base_error.py +++ b/tests/test_base/test_base_error.py @@ -20,9 +20,6 @@ along with this program. If not, see . """ import pytest -import novelwriter - -from PyQt5.QtWidgets import QMessageBox, qApp from mock import causeException @@ -30,18 +27,9 @@ from novelwriter.error import NWErrorMessage, exceptionHandler @pytest.mark.base -def testBaseError_Dialog(qtbot, monkeypatch, fncDir, tmpDir): +def testBaseError_Dialog(qtbot, monkeypatch, nwGUI): """Test the error dialog. """ - # Block message box - monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes) - - qApp.closeAllWindows() - nwGUI = novelwriter.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % tmpDir]) - qtbot.addWidget(nwGUI) - nwGUI.show() - qtbot.wait(20) - nwErr = NWErrorMessage(nwGUI) qtbot.addWidget(nwErr) nwErr.show() @@ -76,19 +64,11 @@ def testBaseError_Dialog(qtbot, monkeypatch, fncDir, tmpDir): @pytest.mark.base -def testBaseError_Handler(qtbot, monkeypatch, fncDir, tmpDir): +def testBaseError_Handler(qtbot, monkeypatch, nwGUI): """Test the error handler. This test doesn'thave any asserts, but it checks that the error handler handles potential exceptions. The test will fail if excpetions are not handled. """ - monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes) - - qApp.closeAllWindows() - nwGUI = novelwriter.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % tmpDir]) - qtbot.addWidget(nwGUI) - nwGUI.show() - qtbot.wait(20) - # Normal shutdown with monkeypatch.context() as mp: mp.setattr(NWErrorMessage, "exec_", lambda *a: None) diff --git a/tests/test_base/test_base_init.py b/tests/test_base/test_base_init.py index 4cebcb6a..2a41ea38 100644 --- a/tests/test_base/test_base_init.py +++ b/tests/test_base/test_base_init.py @@ -28,13 +28,13 @@ from mock import MockGuiMain @pytest.mark.base -def testBaseInit_Launch(caplog, monkeypatch, tmpDir): +def testBaseInit_Launch(caplog, monkeypatch, tmpPath): """Check launching the main GUI. """ monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain) # TestMode Launch - nwGUI = novelwriter.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir]) + nwGUI = novelwriter.main(["--testmode", f"--config={tmpPath}", f"--data={tmpPath}"]) assert isinstance(nwGUI, MockGuiMain) # Darwin Launch @@ -43,7 +43,7 @@ def testBaseInit_Launch(caplog, monkeypatch, tmpDir): novelwriter.CONFIG.osDarwin = True with monkeypatch.context() as mp: mp.setitem(sys.modules, "Foundation", None) - nwGUI = novelwriter.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir]) + nwGUI = novelwriter.main(["--testmode", f"--config={tmpPath}", f"--data={tmpPath}"]) assert isinstance(nwGUI, MockGuiMain) assert "Failed" in caplog.text @@ -55,7 +55,7 @@ def testBaseInit_Launch(caplog, monkeypatch, tmpDir): novelwriter.CONFIG.osWindows = True with monkeypatch.context() as mp: mp.setitem(sys.modules, "ctypes", None) - nwGUI = novelwriter.main(["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir]) + nwGUI = novelwriter.main(["--testmode", f"--config={tmpPath}", f"--data={tmpPath}"]) assert isinstance(nwGUI, MockGuiMain) if not sys.platform.startswith("darwin"): # For some reason, the test doesn't work on macOS @@ -71,19 +71,19 @@ def testBaseInit_Launch(caplog, monkeypatch, tmpDir): monkeypatch.setattr("PyQt5.QtWidgets.QApplication.setOrganizationDomain", lambda *a: None) monkeypatch.setattr("PyQt5.QtWidgets.QApplication.exec_", lambda *a: 0) with pytest.raises(SystemExit) as ex: - novelwriter.main(["--config=%s" % tmpDir, "--data=%s" % tmpDir]) + novelwriter.main([f"--config={tmpPath}", f"--data={tmpPath}"]) assert ex.value.code == 0 # END Test testBaseInit_Launch @pytest.mark.base -def testBaseInit_Options(monkeypatch, tmpDir): +def testBaseInit_Options(monkeypatch, tmpPath): """Test command line options for logging level. """ monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain) monkeypatch.setattr(sys, "argv", [ - "novelWriter.py", "--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir + "novelWriter.py", "--testmode", f"--config={tmpPath}", f"--data={tmpPath}" ]) # Defaults w/None Args @@ -93,20 +93,20 @@ def testBaseInit_Options(monkeypatch, tmpDir): # Defaults nwGUI = novelwriter.main( - ["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir, "--style=Fusion"] + ["--testmode", f"--config={tmpPath}", f"--data={tmpPath}", "--style=Fusion"] ) assert novelwriter.logger.getEffectiveLevel() == logging.WARNING assert nwGUI.closeMain() == "closeMain" # Log Levels nwGUI = novelwriter.main( - ["--testmode", "--info", "--config=%s" % tmpDir, "--data=%s" % tmpDir] + ["--testmode", "--info", f"--config={tmpPath}", f"--data={tmpPath}"] ) assert novelwriter.logger.getEffectiveLevel() == logging.INFO assert nwGUI.closeMain() == "closeMain" nwGUI = novelwriter.main( - ["--testmode", "--debug", "--config=%s" % tmpDir, "--data=%s" % tmpDir] + ["--testmode", "--debug", f"--config={tmpPath}", f"--data={tmpPath}"] ) assert novelwriter.logger.getEffectiveLevel() == logging.DEBUG assert nwGUI.closeMain() == "closeMain" @@ -114,14 +114,14 @@ def testBaseInit_Options(monkeypatch, tmpDir): # Help and Version with pytest.raises(SystemExit) as ex: nwGUI = novelwriter.main( - ["--testmode", "--help", "--config=%s" % tmpDir, "--data=%s" % tmpDir] + ["--testmode", "--help", f"--config={tmpPath}", f"--data={tmpPath}"] ) assert nwGUI.closeMain() == "closeMain" assert ex.value.code == 0 with pytest.raises(SystemExit) as ex: nwGUI = novelwriter.main( - ["--testmode", "--version", "--config=%s" % tmpDir, "--data=%s" % tmpDir] + ["--testmode", "--version", f"--config={tmpPath}", f"--data={tmpPath}"] ) assert nwGUI.closeMain() == "closeMain" assert ex.value.code == 0 @@ -129,14 +129,14 @@ def testBaseInit_Options(monkeypatch, tmpDir): # Invalid options with pytest.raises(SystemExit) as ex: nwGUI = novelwriter.main( - ["--testmode", "--invalid", "--config=%s" % tmpDir, "--data=%s" % tmpDir] + ["--testmode", "--invalid", f"--config={tmpPath}", f"--data={tmpPath}"] ) assert nwGUI.closeMain() == "closeMain" assert ex.value.code == 2 # Project Path nwGUI = novelwriter.main( - ["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir, "sample/"] + ["--testmode", f"--config={tmpPath}", f"--data={tmpPath}", "sample/"] ) assert novelwriter.CONFIG.cmdOpen == "sample/" assert nwGUI.closeMain() == "closeMain" @@ -145,7 +145,7 @@ def testBaseInit_Options(monkeypatch, tmpDir): @pytest.mark.base -def testBaseInit_Imports(caplog, monkeypatch, tmpDir): +def testBaseInit_Imports(caplog, monkeypatch, tmpPath): """Check import error handling. """ monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain) @@ -161,7 +161,7 @@ def testBaseInit_Imports(caplog, monkeypatch, tmpDir): with pytest.raises(SystemExit) as ex: _ = novelwriter.main( - ["--testmode", "--config=%s" % tmpDir, "--data=%s" % tmpDir] + ["--testmode", f"--config={tmpPath}", f"--data={tmpPath}"] ) assert ex.value.code & 4 == 4 # Python version not satisfied diff --git a/tests/test_core/test_core_coretools.py b/tests/test_core/test_core_coretools.py index 39d95a7a..0f9a54f0 100644 --- a/tests/test_core/test_core_coretools.py +++ b/tests/test_core/test_core_coretools.py @@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import uuid import pytest @@ -35,12 +34,12 @@ from novelwriter.core.coretools import DocMerger, DocSplitter, ProjectBuilder @pytest.mark.core -def testCoreTools_DocMerger(monkeypatch, mockGUI, fncDir, outDir, refDir, mockRnd, ipsumText): +def testCoreTools_DocMerger(monkeypatch, mockGUI, fncPath, tstPaths, mockRnd, ipsumText): """Test the DocMerger utility. """ theProject = NWProject(mockGUI) mockRnd.reset() - buildTestProject(theProject, fncDir) + buildTestProject(theProject, fncPath) # Create Files to Merge # ===================== @@ -77,9 +76,9 @@ def testCoreTools_DocMerger(monkeypatch, mockGUI, fncDir, outDir, refDir, mockRn # Merge to New # ============ - saveFile = os.path.join(fncDir, "content", "0000000000014.nwd") - testFile = os.path.join(outDir, "coreDocTools_DocMerger_0000000000014.nwd") - compFile = os.path.join(refDir, "coreDocTools_DocMerger_0000000000014.nwd") + saveFile = fncPath / "content" / "0000000000014.nwd" + testFile = tstPaths.outDir / "coreDocTools_DocMerger_0000000000014.nwd" + compFile = tstPaths.refDir / "coreDocTools_DocMerger_0000000000014.nwd" assert docMerger.newTargetDoc(hChapter1, "All of Chapter 1") == "0000000000014" @@ -92,7 +91,7 @@ def testCoreTools_DocMerger(monkeypatch, mockGUI, fncDir, outDir, refDir, mockRn with monkeypatch.context() as mp: mp.setattr("builtins.open", causeOSError) assert docMerger.writeTargetDoc() is False - assert not os.path.isfile(saveFile) + assert not saveFile.exists() assert docMerger.getError() != "" # Write properly, and compare @@ -103,9 +102,9 @@ def testCoreTools_DocMerger(monkeypatch, mockGUI, fncDir, outDir, refDir, mockRn # Merge into Existing # =================== - saveFile = os.path.join(fncDir, "content", "0000000000010.nwd") - testFile = os.path.join(outDir, "coreDocTools_DocMerger_0000000000010.nwd") - compFile = os.path.join(refDir, "coreDocTools_DocMerger_0000000000010.nwd") + saveFile = fncPath / "content" / "0000000000010.nwd" + testFile = tstPaths.outDir / "coreDocTools_DocMerger_0000000000010.nwd" + compFile = tstPaths.refDir / "coreDocTools_DocMerger_0000000000010.nwd" docMerger.setTargetDoc(hChapter1) @@ -124,12 +123,12 @@ def testCoreTools_DocMerger(monkeypatch, mockGUI, fncDir, outDir, refDir, mockRn @pytest.mark.core -def testCoreTools_DocSplitter(monkeypatch, mockGUI, fncDir, outDir, refDir, mockRnd, ipsumText): +def testCoreTools_DocSplitter(monkeypatch, mockGUI, fncPath, mockRnd, ipsumText): """Test the DocSplitter utility. """ theProject = NWProject(mockGUI) mockRnd.reset() - buildTestProject(theProject, fncDir) + buildTestProject(theProject, fncPath) # Create File to Split # ==================== @@ -264,15 +263,15 @@ def testCoreTools_DocSplitter(monkeypatch, mockGUI, fncDir, outDir, refDir, mock @pytest.mark.core -def testCoreTools_NewMinimal(monkeypatch, fncDir, outDir, refDir, mockGUI, mockRnd): +def testCoreTools_NewMinimal(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd): """Create a new project from a project wizard dictionary. With default setting, creating a Minimal project. """ monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")) - projFile = os.path.join(fncDir, "nwProject.nwx") - testFile = os.path.join(outDir, "coreTools_NewMinimal_nwProject.nwx") - compFile = os.path.join(refDir, "coreTools_NewMinimal_nwProject.nwx") + projFile = fncPath / "nwProject.nwx" + testFile = tstPaths.outDir / "coreTools_NewMinimal_nwProject.nwx" + compFile = tstPaths.refDir / "coreTools_NewMinimal_nwProject.nwx" projBuild = ProjectBuilder(mockGUI) @@ -283,10 +282,10 @@ def testCoreTools_NewMinimal(monkeypatch, fncDir, outDir, refDir, mockGUI, mockR assert projBuild.buildProject("stuff") is False # Try again with a proper path - assert projBuild.buildProject({"projPath": fncDir}) is True + assert projBuild.buildProject({"projPath": fncPath}) is True # Creating the project once more should fail - assert projBuild.buildProject({"projPath": fncDir}) is False + assert projBuild.buildProject({"projPath": fncPath}) is False # Save and close copyfile(projFile, testFile) @@ -296,21 +295,21 @@ def testCoreTools_NewMinimal(monkeypatch, fncDir, outDir, refDir, mockGUI, mockR @pytest.mark.core -def testCoreTools_NewCustomA(monkeypatch, fncDir, outDir, refDir, mockGUI, mockRnd): +def testCoreTools_NewCustomA(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd): """Create a new project from a project wizard dictionary. Custom type with chapters and scenes. """ monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")) - projFile = os.path.join(fncDir, "nwProject.nwx") - testFile = os.path.join(outDir, "coreTools_NewCustomA_nwProject.nwx") - compFile = os.path.join(refDir, "coreTools_NewCustomA_nwProject.nwx") + projFile = fncPath / "nwProject.nwx" + testFile = tstPaths.outDir / "coreTools_NewCustomA_nwProject.nwx" + compFile = tstPaths.refDir / "coreTools_NewCustomA_nwProject.nwx" projData = { "projName": "Test Custom", "projTitle": "Test Novel", "projAuthors": "Jane Doe\nJohn Doh\n", - "projPath": fncDir, + "projPath": fncPath, "popSample": False, "popMinimal": False, "popCustom": True, @@ -334,21 +333,21 @@ def testCoreTools_NewCustomA(monkeypatch, fncDir, outDir, refDir, mockGUI, mockR @pytest.mark.core -def testCoreTools_NewCustomB(monkeypatch, fncDir, outDir, refDir, mockGUI, mockRnd): +def testCoreTools_NewCustomB(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd): """Create a new project from a project wizard dictionary. Custom type without chapters, but with scenes. """ monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")) - projFile = os.path.join(fncDir, "nwProject.nwx") - testFile = os.path.join(outDir, "coreTools_NewCustomB_nwProject.nwx") - compFile = os.path.join(refDir, "coreTools_NewCustomB_nwProject.nwx") + projFile = fncPath / "nwProject.nwx" + testFile = tstPaths.outDir / "coreTools_NewCustomB_nwProject.nwx" + compFile = tstPaths.refDir / "coreTools_NewCustomB_nwProject.nwx" projData = { "projName": "Test Custom", "projTitle": "Test Novel", "projAuthors": "Jane Doe\nJohn Doh\n", - "projPath": fncDir, + "projPath": fncPath, "popSample": False, "popMinimal": False, "popCustom": True, @@ -406,16 +405,15 @@ def testCoreTools_NewSample(monkeypatch, fncPath, tmpConf, tmpPath, mockGUI): outFile.write("foo") assert projBuild.buildProject(projData) is False - os.unlink(dstSample) + dstSample.unlink() # Create a real zip file, and unpack it with ZipFile(dstSample, "w") as zipObj: - zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx") - for docFile in os.listdir(os.path.join(srcSample, "content")): - srcDoc = os.path.join(srcSample, "content", docFile) - zipObj.write(srcDoc, "content/"+docFile) + zipObj.write(srcSample / "nwProject.nwx", "nwProject.nwx") + for docFile in (srcSample / "content").iterdir(): + zipObj.write(docFile, f"content/{docFile.name}") assert projBuild.buildProject(projData) is True - os.unlink(dstSample) + dstSample.unlink() # END Test testCoreTools_NewSample diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index d08577d7..82bf53c8 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -23,7 +23,6 @@ import json import pytest from shutil import copyfile -from pathlib import Path from mock import causeException from tools import C, buildTestProject, cmpFiles, writeFile @@ -35,16 +34,16 @@ from novelwriter.core.project import NWProject @pytest.mark.core -def testCoreIndex_LoadSave(monkeypatch, nwLipsum, mockGUI, tstPaths): +def testCoreIndex_LoadSave(monkeypatch, prjLipsum, mockGUI, tstPaths): """Test core functionality of scaning, saving, loading and checking the index cache file. """ - projFile = Path(nwLipsum) / "meta" / nwFiles.INDEX_FILE + projFile = prjLipsum / "meta" / nwFiles.INDEX_FILE testFile = tstPaths.outDir / "coreIndex_LoadSave_tagsIndex.json" compFile = tstPaths.refDir / "coreIndex_LoadSave_tagsIndex.json" theProject = NWProject(mockGUI) - assert theProject.openProject(nwLipsum) + assert theProject.openProject(prjLipsum) theIndex = NWIndex(theProject) assert repr(theIndex) == "" @@ -196,12 +195,12 @@ def testCoreIndex_ScanThis(mockGUI): @pytest.mark.core -def testCoreIndex_CheckThese(mockGUI, fncDir, mockRnd): +def testCoreIndex_CheckThese(mockGUI, fncPath, mockRnd): """Test the tag checker function checkThese. """ theProject = NWProject(mockGUI) mockRnd.reset() - buildTestProject(theProject, fncDir) + buildTestProject(theProject, fncPath) theIndex = theProject.index theIndex.clearIndex() @@ -274,12 +273,12 @@ def testCoreIndex_CheckThese(mockGUI, fncDir, mockRnd): @pytest.mark.core -def testCoreIndex_ScanText(mockGUI, fncDir, mockRnd): +def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd): """Check the index text scanner. """ theProject = NWProject(mockGUI) mockRnd.reset() - buildTestProject(theProject, fncDir) + buildTestProject(theProject, fncPath) theIndex = theProject.index # Some items for fail to scan tests @@ -486,12 +485,12 @@ def testCoreIndex_ScanText(mockGUI, fncDir, mockRnd): @pytest.mark.core -def testCoreIndex_ExtractData(mockGUI, fncDir, mockRnd): +def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): """Check the index data extraction functions. """ theProject = NWProject(mockGUI) mockRnd.reset() - buildTestProject(theProject, fncDir) + buildTestProject(theProject, fncPath) theIndex = theProject.index theIndex.reIndexHandle(C.hNovelRoot) @@ -940,12 +939,12 @@ def testCoreIndex_TagsIndex(): @pytest.mark.core -def testCoreIndex_ItemIndex(mockGUI, fncDir, mockRnd): +def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd): """Check the ItemIndex class. """ theProject = NWProject(mockGUI) mockRnd.reset() - buildTestProject(theProject, fncDir) + buildTestProject(theProject, fncPath) theProject.index.clearIndex() nHandle = C.hTitlePage diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index 16d2ffe2..6ee0837e 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -31,12 +31,12 @@ from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout @pytest.mark.core -def testCoreItem_Setters(mockGUI, mockRnd, fncDir): +def testCoreItem_Setters(mockGUI, mockRnd, fncPath): """Test all the simple setters for the NWItem class. """ theProject = NWProject(mockGUI) mockRnd.reset() - buildTestProject(theProject, fncDir) + buildTestProject(theProject, fncPath) theItem = NWItem(theProject) statusKeys = ["s000000", "s000001", "s000002", "s000003"] @@ -192,12 +192,12 @@ def testCoreItem_Setters(mockGUI, mockRnd, fncDir): @pytest.mark.core -def testCoreItem_Methods(mockGUI, mockRnd, fncDir): +def testCoreItem_Methods(mockGUI, mockRnd, fncPath): """Test the simple methods of the NWItem class. """ theProject = NWProject(mockGUI) mockRnd.reset() - buildTestProject(theProject, fncDir) + buildTestProject(theProject, fncPath) theItem = NWItem(theProject) # Describe Me diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 377273da..d2931fd8 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -180,6 +180,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd): # Fail on lock file assert theProject._storage.writeLockFile() assert theProject.openProject(fncPath) is False + assert isinstance(theProject.getLockStatus(), list) # Fail to read lockfile (which still opens the project) with monkeypatch.context() as mp: @@ -193,6 +194,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd): assert theProject._storage.writeLockFile() assert theProject.openProject(fncPath, overrideLock=True) is True assert theProject.closeProject() + assert theProject.getLockStatus() is None # Fail getting xml reader with monkeypatch.context() as mp: @@ -625,7 +627,7 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd): @pytest.mark.core -def testCoreProject_OrphanedFiles(mockGUI, nwLipsum): +def testCoreProject_OrphanedFiles(mockGUI, prjLipsum): """Check that files in the content folder that are not tracked in the project XML file are handled correctly by the orphaned files function. It should also restore as much meta data as possible from @@ -633,7 +635,7 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum): """ theProject = NWProject(mockGUI) - assert theProject.openProject(nwLipsum) is True + assert theProject.openProject(prjLipsum) is True assert theProject.tree["636b6aa9b697b"] is None # Add a file with non-existent parent @@ -646,7 +648,7 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum): assert theProject.closeProject() is True # First Item with Meta Data - orphPath = Path(nwLipsum) / "content" / "636b6aa9b697b.nwd" + orphPath = prjLipsum / "content" / "636b6aa9b697b.nwd" writeFile(orphPath, ( "%%~name:[Recovered] Mars\n" "%%~path:5eaea4e8cdee8/636b6aa9b697b\n" @@ -656,22 +658,22 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum): )) # Second Item without Meta Data - orphPath = Path(nwLipsum) / "content" / "736b6aa9b697b.nwd" + orphPath = prjLipsum / "content" / "736b6aa9b697b.nwd" writeFile(orphPath, "\n") # Invalid File Name - tstPath = Path(nwLipsum) / "content" / "636b6aa9b697b.txt" + tstPath = prjLipsum / "content" / "636b6aa9b697b.txt" writeFile(tstPath, "\n") # Invalid File Name - tstPath = Path(nwLipsum) / "content" / "636b6aa9b697bb.nwd" + tstPath = prjLipsum / "content" / "636b6aa9b697bb.nwd" writeFile(tstPath, "\n") # Invalid File Name - tstPath = Path(nwLipsum) / "content" / "abcdefghijklm.nwd" + tstPath = prjLipsum / "content" / "abcdefghijklm.nwd" writeFile(tstPath, "\n") - assert theProject.openProject(nwLipsum) + assert theProject.openProject(prjLipsum) assert theProject.storage.storagePath is not None assert theProject.storage.runtimePath is not None assert theProject.tree["636b6aa9b697bb"] is None @@ -697,7 +699,7 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum): assert oItem.itemType == nwItemType.FILE assert oItem.itemLayout == nwItemLayout.NOTE - assert theProject.saveProject(nwLipsum) + assert theProject.saveProject(prjLipsum) assert theProject.closeProject() # Finally, check that the orphaned files function returns @@ -730,17 +732,17 @@ def testCoreProject_Backup(monkeypatch, mockGUI, fncPath, tmpPath): mockGUI.hasProject = True # Invalid path - theProject.mainConf.backupPath = None + theProject.mainConf._backupPath = None assert theProject.backupProject(doNotify=False) is False # Missing project name - theProject.mainConf.backupPath = str(tmpPath) + theProject.mainConf._backupPath = tmpPath theProject.data.setName("") assert theProject.backupProject(doNotify=False) is False # Valid Settings # ============== - theProject.mainConf.backupPath = str(tmpPath) + theProject.mainConf._backupPath = tmpPath theProject.data.setName("Test Minimal") # Can't make folder diff --git a/tests/test_core/test_core_tohtml.py b/tests/test_core/test_core_tohtml.py index 8880206c..44f16673 100644 --- a/tests/test_core/test_core_tohtml.py +++ b/tests/test_core/test_core_tohtml.py @@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import pytest from tools import readFile @@ -441,7 +440,7 @@ def testCoreToHtml_SpecialCases(mockGUI): @pytest.mark.core -def testCoreToHtml_Complex(mockGUI, fncDir): +def testCoreToHtml_Complex(mockGUI, fncPath): """Test the save method of the ToHtml class. """ theProject = NWProject(mockGUI) @@ -529,7 +528,7 @@ def testCoreToHtml_Complex(mockGUI, fncDir): bodyText="".join(resText).rstrip() ) - saveFile = os.path.join(fncDir, "outFile.htm") + saveFile = fncPath / "outFile.htm" theHtml.saveHTML5(saveFile) assert readFile(saveFile) == htmlDoc diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py index dfe9cf25..f2e939a2 100644 --- a/tests/test_core/test_core_tokenizer.py +++ b/tests/test_core/test_core_tokenizer.py @@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import pytest from tools import C, buildTestProject, readFile @@ -132,12 +131,12 @@ def testCoreToken_Setters(mockGUI): @pytest.mark.core -def testCoreToken_TextOps(monkeypatch, mockGUI, mockRnd, fncDir): +def testCoreToken_TextOps(monkeypatch, mockGUI, mockRnd, fncPath): """Test handling files and text in the Tokenizer class. """ theProject = NWProject(mockGUI) mockRnd.reset() - buildTestProject(theProject, fncDir) + buildTestProject(theProject, fncPath) theProject.data.setLanguage("en") theProject._loadProjectLocalisation() @@ -210,7 +209,7 @@ def testCoreToken_TextOps(monkeypatch, mockGUI, mockRnd, fncDir): assert theToken.theResult == "This is text with escapes: ** ~~ __" # Save File - savePath = os.path.join(fncDir, "dump.nwd") + savePath = fncPath / "dump.nwd" theToken.saveRawMarkdown(savePath) assert readFile(savePath) == ( "# Notes: Plot\n\n" diff --git a/tests/test_core/test_core_tomd.py b/tests/test_core/test_core_tomd.py index 3ec56a6b..aec51566 100644 --- a/tests/test_core/test_core_tomd.py +++ b/tests/test_core/test_core_tomd.py @@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import pytest from tools import readFile @@ -208,7 +207,7 @@ def testCoreToMarkdown_ConvertDirect(mockGUI): @pytest.mark.core -def testCoreToMarkdown_Complex(mockGUI, fncDir): +def testCoreToMarkdown_Complex(mockGUI, fncPath): """Test the save method of the ToMarkdown class. """ theProject = NWProject(mockGUI) @@ -253,7 +252,7 @@ def testCoreToMarkdown_Complex(mockGUI, fncDir): # Check File # ========== - saveFile = os.path.join(fncDir, "outFile.md") + saveFile = fncPath / "outFile.md" theMD.saveMarkdown(saveFile) assert readFile(saveFile) == "".join(resText) diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py index a564a053..cbc9ce17 100644 --- a/tests/test_core/test_core_toodt.py +++ b/tests/test_core/test_core_toodt.py @@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import pytest import zipfile @@ -612,7 +611,7 @@ def testCoreToOdt_ConvertDirect(mockGUI): @pytest.mark.core -def testCoreToOdt_SaveFlat(mockGUI, fncDir, outDir, refDir): +def testCoreToOdt_SaveFlat(mockGUI, fncPath, tstPaths): """Test the document save functions. """ theProject = NWProject(mockGUI) @@ -634,12 +633,12 @@ def testCoreToOdt_SaveFlat(mockGUI, fncDir, outDir, refDir): theDoc.doConvert() theDoc.closeDocument() - flatFile = os.path.join(fncDir, "document.fodt") - testFile = os.path.join(outDir, "coreToOdt_SaveFlat_document.fodt") - compFile = os.path.join(refDir, "coreToOdt_SaveFlat_document.fodt") + flatFile = fncPath / "document.fodt" + testFile = tstPaths.outDir / "coreToOdt_SaveFlat_document.fodt" + compFile = tstPaths.refDir / "coreToOdt_SaveFlat_document.fodt" theDoc.saveFlatXML(flatFile) - assert os.path.isfile(flatFile) + assert flatFile.exists() copyfile(flatFile, testFile) assert cmpFiles(testFile, compFile, [4, 5]) @@ -648,7 +647,7 @@ def testCoreToOdt_SaveFlat(mockGUI, fncDir, outDir, refDir): @pytest.mark.core -def testCoreToOdt_SaveFull(mockGUI, fncDir, outDir, refDir): +def testCoreToOdt_SaveFull(mockGUI, fncPath, tstPaths): """Test the document save functions. """ theProject = NWProject(mockGUI) @@ -667,25 +666,25 @@ def testCoreToOdt_SaveFull(mockGUI, fncDir, outDir, refDir): theDoc.doConvert() theDoc.closeDocument() - fullFile = os.path.join(fncDir, "document.odt") + fullFile = fncPath / "document.odt" theDoc.saveOpenDocText(fullFile) - assert os.path.isfile(fullFile) + assert fullFile.exists() assert zipfile.is_zipfile(fullFile) - maniFile = os.path.join(outDir, "coreToOdt_SaveFull_manifest.xml") - settFile = os.path.join(outDir, "coreToOdt_SaveFull_settings.xml") - contFile = os.path.join(outDir, "coreToOdt_SaveFull_content.xml") - metaFile = os.path.join(outDir, "coreToOdt_SaveFull_meta.xml") - stylFile = os.path.join(outDir, "coreToOdt_SaveFull_styles.xml") + maniFile = tstPaths.outDir / "coreToOdt_SaveFull_manifest.xml" + settFile = tstPaths.outDir / "coreToOdt_SaveFull_settings.xml" + contFile = tstPaths.outDir / "coreToOdt_SaveFull_content.xml" + metaFile = tstPaths.outDir / "coreToOdt_SaveFull_meta.xml" + stylFile = tstPaths.outDir / "coreToOdt_SaveFull_styles.xml" - maniComp = os.path.join(refDir, "coreToOdt_SaveFull_manifest.xml") - settComp = os.path.join(refDir, "coreToOdt_SaveFull_settings.xml") - contComp = os.path.join(refDir, "coreToOdt_SaveFull_content.xml") - metaComp = os.path.join(refDir, "coreToOdt_SaveFull_meta.xml") - stylComp = os.path.join(refDir, "coreToOdt_SaveFull_styles.xml") + maniComp = tstPaths.refDir / "coreToOdt_SaveFull_manifest.xml" + settComp = tstPaths.refDir / "coreToOdt_SaveFull_settings.xml" + contComp = tstPaths.refDir / "coreToOdt_SaveFull_content.xml" + metaComp = tstPaths.refDir / "coreToOdt_SaveFull_meta.xml" + stylComp = tstPaths.refDir / "coreToOdt_SaveFull_styles.xml" - extaxtTo = os.path.join(outDir, "coreToOdt_SaveFull") + extaxtTo = tstPaths.outDir / "coreToOdt_SaveFull" with zipfile.ZipFile(fullFile, mode="r") as theZip: theZip.extract("META-INF/manifest.xml", extaxtTo) @@ -694,17 +693,17 @@ def testCoreToOdt_SaveFull(mockGUI, fncDir, outDir, refDir): theZip.extract("meta.xml", extaxtTo) theZip.extract("styles.xml", extaxtTo) - maniOut = os.path.join(outDir, "coreToOdt_SaveFull", "META-INF", "manifest.xml") - settOut = os.path.join(outDir, "coreToOdt_SaveFull", "settings.xml") - contOut = os.path.join(outDir, "coreToOdt_SaveFull", "content.xml") - metaOut = os.path.join(outDir, "coreToOdt_SaveFull", "meta.xml") - stylOut = os.path.join(outDir, "coreToOdt_SaveFull", "styles.xml") + maniOut = tstPaths.outDir / "coreToOdt_SaveFull" / "META-INF" / "manifest.xml" + settOut = tstPaths.outDir / "coreToOdt_SaveFull" / "settings.xml" + contOut = tstPaths.outDir / "coreToOdt_SaveFull" / "content.xml" + metaOut = tstPaths.outDir / "coreToOdt_SaveFull" / "meta.xml" + stylOut = tstPaths.outDir / "coreToOdt_SaveFull" / "styles.xml" def prettifyXml(inFile, outFile): with open(outFile, mode="wb") as fileStream: fileStream.write( etree.tostring( - etree.parse(inFile), + etree.parse(str(inFile)), pretty_print=True, encoding="utf-8", xml_declaration=True diff --git a/tests/test_dialogs/test_dlg_docmerge.py b/tests/test_dialogs/test_dlg_docmerge.py index 1cf4768c..134ed5a2 100644 --- a/tests/test_dialogs/test_dlg_docmerge.py +++ b/tests/test_dialogs/test_dlg_docmerge.py @@ -29,11 +29,11 @@ from novelwriter.dialogs.docmerge import GuiDocMerge @pytest.mark.gui -def testDlgMerge_Main(qtbot, nwGUI, fncProj, mockRnd): +def testDlgMerge_Main(qtbot, nwGUI, projPath, mockRnd): """Test the merge documents tool. """ # Create a new project - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) # Check that the dialog kan handle invalid items nwMerge = GuiDocMerge(nwGUI, C.hInvalid, [C.hInvalid]) diff --git a/tests/test_dialogs/test_dlg_docsplit.py b/tests/test_dialogs/test_dlg_docsplit.py index 05092c67..9eb296f2 100644 --- a/tests/test_dialogs/test_dlg_docsplit.py +++ b/tests/test_dialogs/test_dlg_docsplit.py @@ -28,13 +28,13 @@ from novelwriter.dialogs.editlabel import GuiEditLabel @pytest.mark.gui -def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): +def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd): """Test the split document tool. """ monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) # Create a new project - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) theProject = nwGUI.theProject projTree = nwGUI.projView.projTree diff --git a/tests/test_dialogs/test_dlg_projload.py b/tests/test_dialogs/test_dlg_projload.py index 7ccf683a..d5019f11 100644 --- a/tests/test_dialogs/test_dlg_projload.py +++ b/tests/test_dialogs/test_dlg_projload.py @@ -20,7 +20,6 @@ along with this program. If not, see . """ import pytest -import os from tools import buildTestProject, getGuiItem @@ -33,10 +32,10 @@ from novelwriter.dialogs.projload import GuiProjectLoad @pytest.mark.gui -def testDlgLoadProject_Main(qtbot, monkeypatch, nwGUI, fncProj): +def testDlgLoadProject_Main(qtbot, monkeypatch, nwGUI, projPath): """Test the load project wizard. """ - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) assert nwGUI.closeProject() monkeypatch.setattr(GuiProjectLoad, "exec_", lambda *a: None) @@ -87,10 +86,10 @@ def testDlgLoadProject_Main(qtbot, monkeypatch, nwGUI, fncProj): nwLoad._doDeleteRecent() assert nwLoad.listBox.topLevelItemCount() == recentCount - 1 - getFile = os.path.join(fncProj, "nwProject.nwx") + getFile = str(projPath / "nwProject.nwx") monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (getFile, None)) qtbot.mouseClick(nwLoad.browseButton, Qt.LeftButton) - assert nwLoad.openPath == fncProj + assert nwLoad.openPath == projPath / "nwProject.nwx" assert nwLoad.openState == nwLoad.OPEN_STATE nwLoad.close() diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projsettings.py index 085e5398..2f51da30 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projsettings.py @@ -82,16 +82,16 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI): @pytest.mark.gui -def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd): +def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): """Test the main tab of the project settings dialog. """ # Mock components monkeypatch.setattr(nwGUI.docEditor.spEnchant, "listDictionaries", lambda: [("en", "none")]) # Create new project - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) mockRnd.reset() - nwGUI.mainConf.backupPath = fncDir + nwGUI.mainConf.backupPath = fncPath # Set some values theProject = nwGUI.theProject @@ -148,7 +148,7 @@ def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd @pytest.mark.gui -def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd): +def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): """Test the status and importance tabs of the project settings dialog. """ @@ -159,8 +159,8 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncDir, fncProj, # Create new project mockRnd.reset() - buildTestProject(nwGUI, fncProj) - nwGUI.mainConf.backupPath = fncDir + buildTestProject(nwGUI, projPath) + nwGUI.mainConf.backupPath = fncPath # Set some values theProject = nwGUI.theProject @@ -350,7 +350,7 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncDir, fncProj, @pytest.mark.gui -def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd): +def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): """Test the auto-replace tab of the project settings dialog. """ monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) @@ -360,8 +360,8 @@ def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mock # Create new project mockRnd.reset() - buildTestProject(nwGUI, fncProj) - nwGUI.mainConf.backupPath = fncDir + buildTestProject(nwGUI, projPath) + nwGUI.mainConf.backupPath = fncPath # Set some values theProject = nwGUI.theProject diff --git a/tests/test_dialogs/test_dlg_wordlist.py b/tests/test_dialogs/test_dlg_wordlist.py index 3a088934..7f8d989c 100644 --- a/tests/test_dialogs/test_dlg_wordlist.py +++ b/tests/test_dialogs/test_dlg_wordlist.py @@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import pytest from PyQt5.QtCore import Qt @@ -33,18 +32,18 @@ from novelwriter.dialogs.wordlist import GuiWordList @pytest.mark.gui -def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, fncProj): +def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, projPath): """test the word list editor. """ - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) monkeypatch.setattr(GuiWordList, "exec_", lambda *a: None) monkeypatch.setattr(GuiWordList, "result", lambda *a: QDialog.Accepted) monkeypatch.setattr(GuiWordList, "accept", lambda *a: None) # Open project - nwGUI.openProject(fncProj) - dictFile = os.path.join(fncProj, "meta", nwFiles.PROJ_DICT) + nwGUI.openProject(projPath) + dictFile = projPath / "meta" / nwFiles.PROJ_DICT # Load the dialog nwGUI.mainMenu.aEditWordList.activate(QAction.Trigger) diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 193fda9e..7f47fd60 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -37,11 +37,11 @@ KEY_DELAY = 1 @pytest.mark.gui -def testGuiEditor_Init(qtbot, nwGUI, fncProj, ipsumText, mockRnd): +def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd): """Test initialising the editor. """ # Open project - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) nwGUI.docEditor.setText("### Lorem Ipsum\n\n%s" % ipsumText[0]) @@ -80,10 +80,10 @@ def testGuiEditor_Init(qtbot, nwGUI, fncProj, ipsumText, mockRnd): @pytest.mark.gui -def testGuiEditor_LoadText(qtbot, monkeypatch, caplog, nwGUI, fncProj, ipsumText, mockRnd): +def testGuiEditor_LoadText(qtbot, monkeypatch, caplog, nwGUI, projPath, ipsumText, mockRnd): """Test loading text into the editor. """ - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True longText = "### Lorem Ipsum\n\n%s" % "\n\n".join(ipsumText*20) @@ -135,10 +135,10 @@ def testGuiEditor_LoadText(qtbot, monkeypatch, caplog, nwGUI, fncProj, ipsumText @pytest.mark.gui -def testGuiEditor_SaveText(qtbot, monkeypatch, caplog, nwGUI, fncProj, ipsumText, mockRnd): +def testGuiEditor_SaveText(qtbot, monkeypatch, caplog, nwGUI, projPath, ipsumText, mockRnd): """Test saving text from the editor. """ - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True # Save Text @@ -179,10 +179,10 @@ def testGuiEditor_SaveText(qtbot, monkeypatch, caplog, nwGUI, fncProj, ipsumText @pytest.mark.gui -def testGuiEditor_MetaData(qtbot, nwGUI, fncProj, mockRnd): +def testGuiEditor_MetaData(qtbot, nwGUI, projPath, mockRnd): """Test extracting various meta data and other values. """ - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True # Get Text @@ -226,13 +226,13 @@ def testGuiEditor_MetaData(qtbot, nwGUI, fncProj, mockRnd): @pytest.mark.gui -def testGuiEditor_Actions(qtbot, nwGUI, fncProj, ipsumText, mockRnd): +def testGuiEditor_Actions(qtbot, nwGUI, projPath, ipsumText, mockRnd): """Test the document actions. This is not an extensive test of the action features, just that the actions are actually called. The various action features are tested when their respective functions are tested. """ - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True theText = "### A Scene\n\n%s" % "\n\n".join(ipsumText) @@ -459,10 +459,10 @@ def testGuiEditor_Actions(qtbot, nwGUI, fncProj, ipsumText, mockRnd): @pytest.mark.gui -def testGuiEditor_Insert(qtbot, monkeypatch, nwGUI, fncProj, ipsumText, mockRnd): +def testGuiEditor_Insert(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd): """Test the document insert functions. """ - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True theText = "### A Scene\n\n%s" % "\n\n".join(ipsumText) @@ -542,10 +542,10 @@ def testGuiEditor_Insert(qtbot, monkeypatch, nwGUI, fncProj, ipsumText, mockRnd) @pytest.mark.gui -def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, fncProj, ipsumText, mockRnd): +def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd): """Test the text manipulation functions. """ - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True theText = "### A Scene\n\n%s" % "\n\n".join(ipsumText) @@ -749,10 +749,10 @@ def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, fncProj, ipsumText @pytest.mark.gui -def testGuiEditor_BlockFormatting(qtbot, monkeypatch, nwGUI, fncProj, ipsumText, mockRnd): +def testGuiEditor_BlockFormatting(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd): """Test the block formatting function. """ - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True theText = "### A Scene\n\n%s" % "\n\n".join(ipsumText) @@ -1062,10 +1062,10 @@ def testGuiEditor_BlockFormatting(qtbot, monkeypatch, nwGUI, fncProj, ipsumText, @pytest.mark.gui -def testGuiEditor_Tags(qtbot, nwGUI, fncProj, ipsumText, mockRnd): +def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd): """Test the document editor tags functionality. """ - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True # Create Scene @@ -1121,7 +1121,7 @@ def testGuiEditor_Tags(qtbot, nwGUI, fncProj, ipsumText, mockRnd): @pytest.mark.gui -def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, fncProj, ipsumText, mockRnd): +def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd): """Test saving text from the editor. """ class MockThreadPool: @@ -1139,7 +1139,7 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, fncProj, ipsumText, mo nwGUI.docEditor.wcTimerDoc.blockSignals(True) nwGUI.docEditor.wcTimerSel.blockSignals(True) - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) # Run on an empty document nwGUI.docEditor._runDocCounter() diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 4ec1ed5e..8c5670a8 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import pytest from tools import C, cmpFiles, buildTestProject, XML_IGNORE, writeFile @@ -67,7 +66,7 @@ def testGuiMain_ProjectBlocker(nwGUI): @pytest.mark.gui -def testGuiMain_NewProject(monkeypatch, nwGUI, fncProj): +def testGuiMain_NewProject(monkeypatch, nwGUI, projPath): """Test creating a new project. """ # No data @@ -79,34 +78,34 @@ def testGuiMain_NewProject(monkeypatch, nwGUI, fncProj): with monkeypatch.context() as mp: nwGUI.hasProject = True mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) - assert nwGUI.newProject(projData={"projPath": fncProj}) is False + assert nwGUI.newProject(projData={"projPath": projPath}) is False # No project path assert nwGUI.newProject(projData={}) is False # Project file already exists - projFile = os.path.join(fncProj, nwFiles.PROJ_FILE) + projFile = projPath / nwFiles.PROJ_FILE writeFile(projFile, "Stuff") - assert nwGUI.newProject(projData={"projPath": fncProj}) is False - os.unlink(projFile) + assert nwGUI.newProject(projData={"projPath": projPath}) is False + projFile.unlink() # An unreachable path should also fail - projPath = os.path.join(fncProj, "stuff", "stuff", "stuff") - assert nwGUI.newProject(projData={"projPath": projPath}) is False + stuffPath = projPath / "stuff" / "stuff" / "stuff" + assert nwGUI.newProject(projData={"projPath": stuffPath}) is False # This one should work just fine - assert nwGUI.newProject(projData={"projPath": fncProj}) is True - assert os.path.isfile(os.path.join(fncProj, nwFiles.PROJ_FILE)) - assert os.path.isdir(os.path.join(fncProj, "content")) + assert nwGUI.newProject(projData={"projPath": projPath}) is True + assert (projPath / nwFiles.PROJ_FILE).is_file() + assert (projPath / "content").is_dir() # END Test testGuiMain_NewProject @pytest.mark.gui -def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): +def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): """Test handling of project tree items based on GUI focus states. """ - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) sHandle = "000000000000f" assert nwGUI.openSelectedItem() is False @@ -153,7 +152,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): @pytest.mark.gui -def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mockRnd): +def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): """Test the document editor. """ monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) @@ -162,7 +161,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) # Create new, save, close project - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) assert nwGUI.saveProject() assert nwGUI.closeProject() @@ -176,14 +175,14 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock assert nwGUI.theProject.data.spellCheck is False # Check the files - projFile = os.path.join(fncProj, "nwProject.nwx") - testFile = os.path.join(outDir, "guiEditor_Main_Initial_nwProject.nwx") - compFile = os.path.join(refDir, "guiEditor_Main_Initial_nwProject.nwx") + projFile = projPath / "nwProject.nwx" + testFile = tstPaths.outDir / "guiEditor_Main_Initial_nwProject.nwx" + compFile = tstPaths.refDir / "guiEditor_Main_Initial_nwProject.nwx" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) # Re-open project - assert nwGUI.openProject(fncProj) + assert nwGUI.openProject(projPath) # Check that we loaded the data assert len(nwGUI.theProject.tree) == 8 @@ -494,33 +493,33 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock assert nwGUI.saveProject() # Check the files - projFile = os.path.join(fncProj, "nwProject.nwx") - testFile = os.path.join(outDir, "guiEditor_Main_Final_nwProject.nwx") - compFile = os.path.join(refDir, "guiEditor_Main_Final_nwProject.nwx") + projFile = projPath / "nwProject.nwx" + testFile = tstPaths.outDir / "guiEditor_Main_Final_nwProject.nwx" + compFile = tstPaths.refDir / "guiEditor_Main_Final_nwProject.nwx" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile, ignoreStart=(*XML_IGNORE, ". """ import pytest -import os -from PyQt5.QtCore import Qt from PyQt5.QtGui import QTextCursor, QTextBlock +from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox from tools import C, writeFile, buildTestProject @@ -422,10 +421,10 @@ def testGuiMenu_ContextMenus(qtbot, nwGUI, nwLipsum): @pytest.mark.gui -def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd): +def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): """Test the Insert menu. """ - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) assert nwGUI.projView.projTree._getTreeItem(C.hSceneDoc) is not None assert nwGUI.openDocument(C.hSceneDoc) is True @@ -626,8 +625,8 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd): assert not nwGUI.importDocument() # Then a valid path, but bot a file that exists - theFile = os.path.join(fncDir, "import.txt") - monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (theFile, "")) + theFile = fncPath / "import.txt" + monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(theFile), "")) assert not nwGUI.importDocument() # Create the file and try again, but with no target document open @@ -666,7 +665,7 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd): theBits = theMessage.split("
") assert len(theBits) == 2 assert theBits[0] == "The currently open file is saved in:" - assert theBits[1] == os.path.join(fncProj, "content", "000000000000f.nwd") + assert theBits[1] == str(projPath / "content" / "000000000000f.nwd") # qtbot.stop() diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index cfaeecc4..20c38d03 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -35,12 +35,12 @@ from novelwriter.dialogs.editlabel import GuiEditLabel @pytest.mark.gui -def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): +def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): """Test navigating the novel tree. """ monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) nwGUI.switchFocus(nwWidget.TREE) nwGUI.projView.projTree.clearSelection() diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 77d9e739..23f61c5b 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -32,12 +32,11 @@ from novelwriter.enum import nwItemClass, nwOutline, nwView @pytest.mark.gui -def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir): +def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, projPath): """Test the outline view. """ # Create a project - prjDir = os.path.join(fncDir, "project") - buildTestProject(nwGUI, prjDir) + buildTestProject(nwGUI, projPath) nwGUI.rebuildIndex() nwGUI._changeView(nwView.OUTLINE) diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 13d27382..cbf607d8 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import pytest from mock import causeOSError @@ -36,7 +35,7 @@ from novelwriter.dialogs.editlabel import GuiEditLabel @pytest.mark.gui -def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd): +def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd): """Test adding and removing items from the project tree. """ monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) @@ -49,8 +48,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) assert projView.projTree.newTreeItem(nwItemType.FILE) is False # Create a project - prjDir = os.path.join(fncDir, "project") - buildTestProject(nwGUI, prjDir) + buildTestProject(nwGUI, projPath) # No itemType set projView.projTree.clearSelection() @@ -168,7 +166,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd) @pytest.mark.gui -def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): +def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): """Test adding and removing items from the project tree. """ monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) @@ -180,8 +178,7 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): assert projView.projTree.moveTreeItem(1) is False # Create a project - prjDir = os.path.join(fncDir, "project") - buildTestProject(nwGUI, prjDir) + buildTestProject(nwGUI, projPath) # Move Documents # ============== @@ -279,7 +276,7 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): @pytest.mark.gui -def testGuiProjTree_RequestDeleteItem(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd): +def testGuiProjTree_RequestDeleteItem(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd): """Test external requests for removing items from project tree. """ monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) @@ -291,8 +288,7 @@ def testGuiProjTree_RequestDeleteItem(qtbot, caplog, monkeypatch, nwGUI, fncDir, assert projView.requestDeleteItem() is False # Create a project - prjDir = os.path.join(fncDir, "project") - buildTestProject(nwGUI, prjDir) + buildTestProject(nwGUI, projPath) # Try emptying the trash already now, when there is no trash folder assert projView.emptyTrash() is False @@ -363,7 +359,7 @@ def testGuiProjTree_RequestDeleteItem(qtbot, caplog, monkeypatch, nwGUI, fncDir, @pytest.mark.gui -def testGuiProjTree_MoveItemToTrash(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd): +def testGuiProjTree_MoveItemToTrash(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd): """Test moving items to Trash. """ monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) @@ -372,8 +368,7 @@ def testGuiProjTree_MoveItemToTrash(qtbot, caplog, monkeypatch, nwGUI, fncDir, m projTree = nwGUI.projView.projTree # Create a project - prjDir = os.path.join(fncDir, "project") - buildTestProject(nwGUI, prjDir) + buildTestProject(nwGUI, projPath) # Invalid item caplog.clear() @@ -417,7 +412,7 @@ def testGuiProjTree_MoveItemToTrash(qtbot, caplog, monkeypatch, nwGUI, fncDir, m @pytest.mark.gui -def testGuiProjTree_PermanentlyDeleteItem(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd): +def testGuiProjTree_PermanentlyDeleteItem(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd): """Test permanently deleting items. """ monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) @@ -426,8 +421,7 @@ def testGuiProjTree_PermanentlyDeleteItem(qtbot, caplog, monkeypatch, nwGUI, fnc projTree = nwGUI.projView.projTree # Create a project - prjDir = os.path.join(fncDir, "project") - buildTestProject(nwGUI, prjDir) + buildTestProject(nwGUI, projPath) # Invalid item caplog.clear() @@ -470,7 +464,7 @@ def testGuiProjTree_PermanentlyDeleteItem(qtbot, caplog, monkeypatch, nwGUI, fnc @pytest.mark.gui -def testGuiProjTree_EmptyTrash(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd): +def testGuiProjTree_EmptyTrash(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd): """Test emptying Trash. """ monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) @@ -484,8 +478,7 @@ def testGuiProjTree_EmptyTrash(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRn assert "No project open" in caplog.text # Create a project - prjDir = os.path.join(fncDir, "project") - buildTestProject(nwGUI, prjDir) + buildTestProject(nwGUI, projPath) # No Trash folder assert projTree.emptyTrash() is False @@ -524,7 +517,7 @@ def testGuiProjTree_EmptyTrash(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRn @pytest.mark.gui -def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): +def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd): """Test the building of the project tree context menu. All this does is test that the menu builds. It doesn't open the actual menu, """ @@ -532,8 +525,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): monkeypatch.setattr(QMenu, "exec_", lambda *a: None) # Create a project - prjDir = os.path.join(fncDir, "project") - buildTestProject(nwGUI, prjDir) + buildTestProject(nwGUI, projPath) # Handles for new objects hCharNote = "0000000000011" @@ -643,7 +635,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): @pytest.mark.gui -def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, ipsumText): +def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd, ipsumText): """Test the merge document function. """ mergeData = {} @@ -654,8 +646,7 @@ def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, i monkeypatch.setattr(GuiDocMerge, "getData", lambda *a: mergeData) # Create a project - prjDir = os.path.join(fncDir, "project") - buildTestProject(nwGUI, prjDir) + buildTestProject(nwGUI, projPath) theProject = nwGUI.theProject projTree = nwGUI.projView.projTree @@ -746,7 +737,7 @@ def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, i @pytest.mark.gui -def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, ipsumText): +def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd, ipsumText): """Test the split document function. """ splitData = {} @@ -758,8 +749,7 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, ip monkeypatch.setattr(GuiDocSplit, "getData", lambda *a: (splitData, splitText)) # Create a project - prjDir = os.path.join(fncDir, "project") - buildTestProject(nwGUI, prjDir) + buildTestProject(nwGUI, projPath) theProject = nwGUI.theProject projTree = nwGUI.projView.projTree @@ -828,13 +818,13 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, ip assert projTree._splitDocument(hSplitDoc) is True for tHandle in fstSet: assert tHandle in theProject.tree - assert not os.path.isfile(os.path.join(prjDir, "content", f"{tHandle}.nwd")) + assert not (projPath / "content" / f"{tHandle}.nwd").is_file() # Writing succeeds assert projTree._splitDocument(hSplitDoc) is True for tHandle in sndSet: assert tHandle in theProject.tree - assert os.path.isfile(os.path.join(prjDir, "content", f"{tHandle}.nwd")) + assert (projPath / "content" / f"{tHandle}.nwd").is_file() # Add to a folder and move source to trash splitData["intoFolder"] = True @@ -843,7 +833,7 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, ip assert "0000000000029" in theProject.tree # The folder for tHandle in trdSet: assert tHandle in theProject.tree - assert os.path.isfile(os.path.join(prjDir, "content", f"{tHandle}.nwd")) + assert (projPath / "content" / f"{tHandle}.nwd").is_file() assert theProject.tree.isTrash(hSplitDoc) is True @@ -858,13 +848,12 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, ip @pytest.mark.gui -def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI, fncDir, mockRnd): +def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI, projPath, mockRnd): """Test various parts of the project tree class not covered by other tests. """ # Create a project - prjDir = os.path.join(fncDir, "project") - buildTestProject(nwGUI, prjDir) + buildTestProject(nwGUI, projPath) projView = nwGUI.projView projTree = nwGUI.projView.projTree diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py index 3f04acda..a057fb36 100644 --- a/tests/test_gui/test_gui_statusbar.py +++ b/tests/test_gui/test_gui_statusbar.py @@ -28,10 +28,10 @@ from novelwriter.enum import nwState @pytest.mark.gui -def testGuiStatusBar_Main(qtbot, nwGUI, fncProj, mockRnd): +def testGuiStatusBar_Main(qtbot, nwGUI, projPath, mockRnd): """Test the the various features of the status bar. """ - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) cHandle = nwGUI.theProject.newFile("A Note", C.hCharRoot) newDoc = nwGUI.theProject.storage.getDocument(cHandle) newDoc.writeDocument("# A Note\n\n") diff --git a/tests/test_tools/test_tools_build.py b/tests/test_tools/test_tools_build.py index 5cb93229..33bd8ecc 100644 --- a/tests/test_tools/test_tools_build.py +++ b/tests/test_tools/test_tools_build.py @@ -20,10 +20,8 @@ along with this program. If not, see . """ import pytest -import os from shutil import copyfile -from pathlib import Path from tools import cmpFiles, getGuiItem @@ -34,7 +32,7 @@ from novelwriter.tools import GuiBuildNovel @pytest.mark.gui -def testToolBuild_Main(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir): +def testToolBuild_Main(qtbot, monkeypatch, nwGUI, prjLipsum, tstPaths): """Test the build tool. """ # Block message box @@ -45,7 +43,7 @@ def testToolBuild_Main(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir): assert getGuiItem("GuiBuildNovel") is None # Open a project - assert nwGUI.openProject(nwLipsum) + assert nwGUI.openProject(prjLipsum) # Open the tool nwGUI.mainMenu.aBuildProject.activate(QAction.Trigger) @@ -69,41 +67,41 @@ def testToolBuild_Main(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir): assert not nwBuild._saveDocument(nwBuild.FMT_NWD) # Default Settings - nwGUI.mainConf._lastPath = Path(nwLipsum) + nwGUI.mainConf._lastPath = prjLipsum qtbot.mouseClick(nwBuild.buildNovel, Qt.LeftButton) assert nwBuild._saveDocument(nwBuild.FMT_NWD) - projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd") - testFile = os.path.join(outDir, "guiBuild_Tool_Step1_Lorem_Ipsum.nwd") - compFile = os.path.join(refDir, "guiBuild_Tool_Step1_Lorem_Ipsum.nwd") + projFile = prjLipsum / "Lorem Ipsum.nwd" + testFile = tstPaths.outDir / "guiBuild_Tool_Step1_Lorem_Ipsum.nwd" + compFile = tstPaths.refDir / "guiBuild_Tool_Step1_Lorem_Ipsum.nwd" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) assert nwBuild._saveDocument(nwBuild.FMT_HTM) - projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm") - testFile = os.path.join(outDir, "guiBuild_Tool_Step1_Lorem_Ipsum.htm") - compFile = os.path.join(refDir, "guiBuild_Tool_Step1_Lorem_Ipsum.htm") + projFile = prjLipsum / "Lorem Ipsum.htm" + testFile = tstPaths.outDir / "guiBuild_Tool_Step1_Lorem_Ipsum.htm" + compFile = tstPaths.refDir / "guiBuild_Tool_Step1_Lorem_Ipsum.htm" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) assert nwBuild._saveDocument(nwBuild.FMT_MD) - projFile = os.path.join(nwLipsum, "Lorem Ipsum.md") - testFile = os.path.join(outDir, "guiBuild_Tool_Step1_Lorem_Ipsum.md") - compFile = os.path.join(refDir, "guiBuild_Tool_Step1_Lorem_Ipsum.md") + projFile = prjLipsum / "Lorem Ipsum.md" + testFile = tstPaths.outDir / "guiBuild_Tool_Step1_Lorem_Ipsum.md" + compFile = tstPaths.refDir / "guiBuild_Tool_Step1_Lorem_Ipsum.md" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) assert nwBuild._saveDocument(nwBuild.FMT_GH) - projFile = os.path.join(nwLipsum, "Lorem Ipsum.md") - testFile = os.path.join(outDir, "guiBuild_Tool_Step1G_Lorem_Ipsum.md") - compFile = os.path.join(refDir, "guiBuild_Tool_Step1G_Lorem_Ipsum.md") + projFile = prjLipsum / "Lorem Ipsum.md" + testFile = tstPaths.outDir / "guiBuild_Tool_Step1G_Lorem_Ipsum.md" + compFile = tstPaths.refDir / "guiBuild_Tool_Step1G_Lorem_Ipsum.md" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) assert nwBuild._saveDocument(nwBuild.FMT_FODT) - projFile = os.path.join(nwLipsum, "Lorem Ipsum.fodt") - testFile = os.path.join(outDir, "guiBuild_Tool_Step1_Lorem_Ipsum.fodt") - compFile = os.path.join(refDir, "guiBuild_Tool_Step1_Lorem_Ipsum.fodt") + projFile = prjLipsum / "Lorem Ipsum.fodt" + testFile = tstPaths.outDir / "guiBuild_Tool_Step1_Lorem_Ipsum.fodt" + compFile = tstPaths.refDir / "guiBuild_Tool_Step1_Lorem_Ipsum.fodt" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile, [4, 5]) @@ -124,30 +122,30 @@ def testToolBuild_Main(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir): qtbot.mouseClick(nwBuild.buildNovel, Qt.LeftButton) assert nwBuild._saveDocument(nwBuild.FMT_NWD) - projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd") - testFile = os.path.join(outDir, "guiBuild_Tool_Step2_Lorem_Ipsum.nwd") - compFile = os.path.join(refDir, "guiBuild_Tool_Step2_Lorem_Ipsum.nwd") + projFile = prjLipsum / "Lorem Ipsum.nwd" + testFile = tstPaths.outDir / "guiBuild_Tool_Step2_Lorem_Ipsum.nwd" + compFile = tstPaths.refDir / "guiBuild_Tool_Step2_Lorem_Ipsum.nwd" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) assert nwBuild._saveDocument(nwBuild.FMT_HTM) - projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm") - testFile = os.path.join(outDir, "guiBuild_Tool_Step2_Lorem_Ipsum.htm") - compFile = os.path.join(refDir, "guiBuild_Tool_Step2_Lorem_Ipsum.htm") + projFile = prjLipsum / "Lorem Ipsum.htm" + testFile = tstPaths.outDir / "guiBuild_Tool_Step2_Lorem_Ipsum.htm" + compFile = tstPaths.refDir / "guiBuild_Tool_Step2_Lorem_Ipsum.htm" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) assert nwBuild._saveDocument(nwBuild.FMT_MD) - projFile = os.path.join(nwLipsum, "Lorem Ipsum.md") - testFile = os.path.join(outDir, "guiBuild_Tool_Step2_Lorem_Ipsum.md") - compFile = os.path.join(refDir, "guiBuild_Tool_Step2_Lorem_Ipsum.md") + projFile = prjLipsum / "Lorem Ipsum.md" + testFile = tstPaths.outDir / "guiBuild_Tool_Step2_Lorem_Ipsum.md" + compFile = tstPaths.refDir / "guiBuild_Tool_Step2_Lorem_Ipsum.md" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) assert nwBuild._saveDocument(nwBuild.FMT_FODT) - projFile = os.path.join(nwLipsum, "Lorem Ipsum.fodt") - testFile = os.path.join(outDir, "guiBuild_Tool_Step2_Lorem_Ipsum.fodt") - compFile = os.path.join(refDir, "guiBuild_Tool_Step2_Lorem_Ipsum.fodt") + projFile = prjLipsum / "Lorem Ipsum.fodt" + testFile = tstPaths.outDir / "guiBuild_Tool_Step2_Lorem_Ipsum.fodt" + compFile = tstPaths.refDir / "guiBuild_Tool_Step2_Lorem_Ipsum.fodt" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile, [4, 5]) @@ -158,30 +156,30 @@ def testToolBuild_Main(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir): # Save files that can be compared assert nwBuild._saveDocument(nwBuild.FMT_NWD) - projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd") - testFile = os.path.join(outDir, "guiBuild_Tool_Step3_Lorem_Ipsum.nwd") - compFile = os.path.join(refDir, "guiBuild_Tool_Step3_Lorem_Ipsum.nwd") + projFile = prjLipsum / "Lorem Ipsum.nwd" + testFile = tstPaths.outDir / "guiBuild_Tool_Step3_Lorem_Ipsum.nwd" + compFile = tstPaths.refDir / "guiBuild_Tool_Step3_Lorem_Ipsum.nwd" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) assert nwBuild._saveDocument(nwBuild.FMT_HTM) - projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm") - testFile = os.path.join(outDir, "guiBuild_Tool_Step3_Lorem_Ipsum.htm") - compFile = os.path.join(refDir, "guiBuild_Tool_Step3_Lorem_Ipsum.htm") + projFile = prjLipsum / "Lorem Ipsum.htm" + testFile = tstPaths.outDir / "guiBuild_Tool_Step3_Lorem_Ipsum.htm" + compFile = tstPaths.refDir / "guiBuild_Tool_Step3_Lorem_Ipsum.htm" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) assert nwBuild._saveDocument(nwBuild.FMT_MD) - projFile = os.path.join(nwLipsum, "Lorem Ipsum.md") - testFile = os.path.join(outDir, "guiBuild_Tool_Step3_Lorem_Ipsum.md") - compFile = os.path.join(refDir, "guiBuild_Tool_Step3_Lorem_Ipsum.md") + projFile = prjLipsum / "Lorem Ipsum.md" + testFile = tstPaths.outDir / "guiBuild_Tool_Step3_Lorem_Ipsum.md" + compFile = tstPaths.refDir / "guiBuild_Tool_Step3_Lorem_Ipsum.md" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) assert nwBuild._saveDocument(nwBuild.FMT_FODT) - projFile = os.path.join(nwLipsum, "Lorem Ipsum.fodt") - testFile = os.path.join(outDir, "guiBuild_Tool_Step3_Lorem_Ipsum.fodt") - compFile = os.path.join(refDir, "guiBuild_Tool_Step3_Lorem_Ipsum.fodt") + projFile = prjLipsum / "Lorem Ipsum.fodt" + testFile = tstPaths.outDir / "guiBuild_Tool_Step3_Lorem_Ipsum.fodt" + compFile = tstPaths.refDir / "guiBuild_Tool_Step3_Lorem_Ipsum.fodt" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile, [4, 5]) @@ -199,43 +197,43 @@ def testToolBuild_Main(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir): # Save files that can be compared assert nwBuild._saveDocument(nwBuild.FMT_NWD) - projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd") - testFile = os.path.join(outDir, "guiBuild_Tool_Step4_Lorem_Ipsum.nwd") - compFile = os.path.join(refDir, "guiBuild_Tool_Step4_Lorem_Ipsum.nwd") + projFile = prjLipsum / "Lorem Ipsum.nwd" + testFile = tstPaths.outDir / "guiBuild_Tool_Step4_Lorem_Ipsum.nwd" + compFile = tstPaths.refDir / "guiBuild_Tool_Step4_Lorem_Ipsum.nwd" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) assert nwBuild._saveDocument(nwBuild.FMT_HTM) - projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm") - testFile = os.path.join(outDir, "guiBuild_Tool_Step4_Lorem_Ipsum.htm") - compFile = os.path.join(refDir, "guiBuild_Tool_Step4_Lorem_Ipsum.htm") + projFile = prjLipsum / "Lorem Ipsum.htm" + testFile = tstPaths.outDir / "guiBuild_Tool_Step4_Lorem_Ipsum.htm" + compFile = tstPaths.refDir / "guiBuild_Tool_Step4_Lorem_Ipsum.htm" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile) # Check the JSON files too at this stage assert nwBuild._saveDocument(nwBuild.FMT_JSON_H) - projFile = os.path.join(nwLipsum, "Lorem Ipsum.json") - testFile = os.path.join(outDir, "guiBuild_Tool_Step4H_Lorem_Ipsum.json") - compFile = os.path.join(refDir, "guiBuild_Tool_Step4H_Lorem_Ipsum.json") + projFile = prjLipsum / "Lorem Ipsum.json" + testFile = tstPaths.outDir / "guiBuild_Tool_Step4H_Lorem_Ipsum.json" + compFile = tstPaths.refDir / "guiBuild_Tool_Step4H_Lorem_Ipsum.json" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile, [8]) assert nwBuild._saveDocument(nwBuild.FMT_JSON_M) - projFile = os.path.join(nwLipsum, "Lorem Ipsum.json") - testFile = os.path.join(outDir, "guiBuild_Tool_Step4M_Lorem_Ipsum.json") - compFile = os.path.join(refDir, "guiBuild_Tool_Step4M_Lorem_Ipsum.json") + projFile = prjLipsum / "Lorem Ipsum.json" + testFile = tstPaths.outDir / "guiBuild_Tool_Step4M_Lorem_Ipsum.json" + compFile = tstPaths.refDir / "guiBuild_Tool_Step4M_Lorem_Ipsum.json" copyfile(projFile, testFile) assert cmpFiles(testFile, compFile, [8]) # Since odt and fodt is built by the same code, we don't check the # output. but just that the different format can be written as well assert nwBuild._saveDocument(nwBuild.FMT_ODT) - assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.odt")) + assert (prjLipsum / "Lorem Ipsum.odt").is_file() # Print to PDF if not nwGUI.mainConf.osDarwin: assert nwBuild._saveDocument(nwBuild.FMT_PDF) - assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.pdf")) + assert (prjLipsum / "Lorem Ipsum.pdf").is_file() # Close the build tool htmlText = nwBuild.htmlText diff --git a/tests/test_tools/test_tools_lipsum.py b/tests/test_tools/test_tools_lipsum.py index 0bd07f97..409a168b 100644 --- a/tests/test_tools/test_tools_lipsum.py +++ b/tests/test_tools/test_tools_lipsum.py @@ -29,7 +29,7 @@ from novelwriter.tools import GuiLipsum @pytest.mark.gui -def testToolLipsum_Main(qtbot, nwGUI, fncProj, mockRnd): +def testToolLipsum_Main(qtbot, nwGUI, projPath, mockRnd): """Test the Lorem Ipsum tool. """ # Check that we cannot open when there is no project @@ -37,7 +37,7 @@ def testToolLipsum_Main(qtbot, nwGUI, fncProj, mockRnd): assert getGuiItem("GuiLipsum") is None # Create a new project - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True assert len(nwGUI.docEditor.getText()) == 15 diff --git a/tests/test_tools/test_tools_projwizard.py b/tests/test_tools/test_tools_projwizard.py index ed9d8a58..336e4b1b 100644 --- a/tests/test_tools/test_tools_projwizard.py +++ b/tests/test_tools/test_tools_projwizard.py @@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import sys import pytest @@ -37,7 +36,7 @@ from novelwriter.tools.projwizard import ( @pytest.mark.gui @pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin") -def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, fncProj): +def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, projPath): """Test the launch of the project wizard. Disabled for macOS because the test segfaults on QWizard.show() """ @@ -45,7 +44,7 @@ def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, fncProj): # ======================== # New with a project open should cause an error - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) with monkeypatch.context() as mp: mp.setattr(nwGUI, "closeProject", lambda *a: False) assert nwGUI.newProject() is False @@ -61,7 +60,7 @@ def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, fncProj): assert nwGUI.newProject() is False # Now, with a non-empty folder - mp.setattr(nwGUI, "showNewProjectDialog", lambda *a: {"projPath": fncProj}) + mp.setattr(nwGUI, "showNewProjectDialog", lambda *a: {"projPath": projPath}) assert nwGUI.newProject() is False # Test the Wizard Launching @@ -96,7 +95,7 @@ def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, fncProj): @pytest.mark.gui @pytest.mark.parametrize("prjType", ["minimal", "custom1", "custom2", "sample"]) @pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin") -def testToolProjectWizard_Run(qtbot, monkeypatch, nwGUI, fncDir, prjType): +def testToolProjectWizard_Run(qtbot, monkeypatch, nwGUI, fncPath, prjType): """Test the new project wizard with a set of selection scenarios. """ monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *a: None) @@ -130,12 +129,12 @@ def testToolProjectWizard_Run(qtbot, monkeypatch, nwGUI, fncDir, prjType): assert storagePage.errLabel.text() == "" # Set an invalid path - storagePage.projPath.setText(os.path.join(fncDir, "not", "a", "path")) + storagePage.projPath.setText(str(fncPath / "not" / "a" / "path")) assert not nwWiz.button(QWizard.NextButton).isEnabled() assert storagePage.errLabel.text().startswith("Error") # Set an existing path - storagePage.projPath.setText(fncDir) + storagePage.projPath.setText(str(fncPath)) assert not nwWiz.button(QWizard.NextButton).isEnabled() assert storagePage.errLabel.text().startswith("Error") @@ -146,12 +145,12 @@ def testToolProjectWizard_Run(qtbot, monkeypatch, nwGUI, fncDir, prjType): assert storagePage.errLabel.text() == "" # Let the browse feature handle it - projPath = os.path.join(fncDir, "Test Wizard") + projPath = fncPath / "Test Wizard" with monkeypatch.context() as mp: - mp.setattr(QFileDialog, "getExistingDirectory", lambda *a, **k: fncDir) + mp.setattr(QFileDialog, "getExistingDirectory", lambda *a, **k: str(fncPath)) qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100) - assert storagePage.projPath.text() == projPath + assert storagePage.projPath.text() == str(projPath) assert storagePage.errLabel.text() == "" # Setting projPath should activate the button @@ -216,7 +215,7 @@ def testToolProjectWizard_Run(qtbot, monkeypatch, nwGUI, fncDir, prjType): assert projData["projName"] == "Test Wizard" assert projData["projTitle"] == "My Novel" assert projData["projAuthors"] == "Jane Doe" - assert projData["projPath"] == projPath + assert projData["projPath"] == str(projPath) assert projData["popMinimal"] == prjType.startswith("minimal") assert projData["popCustom"] == prjType.startswith("custom") assert projData["popSample"] == prjType.startswith("sample") diff --git a/tests/test_tools/test_tools_writingstats.py b/tests/test_tools/test_tools_writingstats.py index 60f59750..a02d386a 100644 --- a/tests/test_tools/test_tools_writingstats.py +++ b/tests/test_tools/test_tools_writingstats.py @@ -19,9 +19,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import pytest import json -import os +import pytest from mock import causeOSError from tools import getGuiItem, writeFile, buildTestProject @@ -34,14 +33,14 @@ from novelwriter.constants import nwFiles @pytest.mark.gui -def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj): +def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath): """Test the full writing stats tool. """ # Create a project to work on - buildTestProject(nwGUI, fncProj) + buildTestProject(nwGUI, projPath) qtbot.wait(100) assert nwGUI.saveProject() - sessFile = os.path.join(fncProj, "meta", nwFiles.SESS_STATS) + sessFile = projPath / "meta" / nwFiles.SESS_STATS # Open the Writing Stats dialog nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger) @@ -54,7 +53,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj): # ============ # No initial logfile - assert not os.path.isfile(sessFile) + assert not sessFile.is_file() assert not sessLog._loadLogFile() # Make a test log file @@ -66,7 +65,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj): "2020-01-03 21:30:00 2020-01-03 21:30:15 125 5\n" "2020-01-06 21:00:00 2020-01-06 21:00:10 125 5\n" )) - assert os.path.isfile(sessFile) + assert sessFile.is_file() assert sessLog._loadLogFile() assert sessLog.wordOffset == 123 assert len(sessLog.logData) == 4 @@ -110,9 +109,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj): assert not sessLog._saveData(None) # Make the save succeed - monkeypatch.setattr("os.path.expanduser", lambda *a: fncDir) monkeypatch.setattr(QFileDialog, "getSaveFileName", lambda ss, tt, pp, options: (pp, "")) - sessLog.listBox.sortByColumn(sessLog.C_TIME, 0) assert sessLog.novelWords.text() == "{:n}".format(600) @@ -135,7 +132,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj): qtbot.wait(100) # Check the exported files - jsonStats = os.path.join(fncDir, "sessionStats.json") + jsonStats = fncPath / "sessionStats.json" with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.load(inFile) @@ -174,7 +171,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj): qtbot.mouseClick(sessLog.incNovel, Qt.LeftButton) assert sessLog._saveData(sessLog.FMT_JSON) - jsonStats = os.path.join(fncDir, "sessionStats.json") + jsonStats = fncPath / "sessionStats.json" with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.loads(inFile.read()) @@ -220,7 +217,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj): qtbot.mouseClick(sessLog.incNotes, Qt.LeftButton) assert sessLog._saveData(sessLog.FMT_JSON) - jsonStats = os.path.join(fncDir, "sessionStats.json") + jsonStats = fncPath / "sessionStats.json" with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.load(inFile) @@ -268,7 +265,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj): # qtbot.stop() - jsonStats = os.path.join(fncDir, "sessionStats.json") + jsonStats = fncPath / "sessionStats.json" with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.load(inFile) @@ -298,7 +295,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj): qtbot.mouseClick(sessLog.hideZeros, Qt.LeftButton) assert sessLog._saveData(sessLog.FMT_JSON) - jsonStats = os.path.join(fncDir, "sessionStats.json") + jsonStats = fncPath / "sessionStats.json" with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.load(inFile) @@ -351,7 +348,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj): qtbot.mouseClick(sessLog.groupByDay, Qt.LeftButton) assert sessLog._saveData(sessLog.FMT_JSON) - jsonStats = os.path.join(fncDir, "sessionStats.json") + jsonStats = fncPath / "sessionStats.json" with open(jsonStats, mode="r", encoding="utf-8") as inFile: jsonData = json.load(inFile) diff --git a/tests/tools.py b/tests/tools.py index c2511b28..0883dc14 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -19,10 +19,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import os import time import shutil +from pathlib import Path + from PyQt5.QtWidgets import qApp XML_IGNORE = (" Date: Wed, 9 Nov 2022 22:51:12 +0100 Subject: [PATCH 9/9] Fix windows vs posix path issue in test --- tests/test_dialogs/test_dlg_preferences.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index fbf9af9b..193987f4 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -225,7 +225,7 @@ def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, fncPath, tstPaths): ignTuple = ( "timestamp", "guifont", "lastnotes", "guilang", "geometry", "preferences", "projcols", "mainpane", "docpane", "viewpane", - "outlinepane", "textfont", "textsize", "lastpath" + "outlinepane", "textfont", "textsize", "lastpath", "backuppath" ) assert cmpFiles(testFile, compFile, ignoreStart=ignTuple)