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)