Refactor config class, and switch to pathlib (#1228)

This commit is contained in:
Veronica Berglyd Olsen
2022-11-09 23:03:03 +01:00
committed by GitHub
49 changed files with 1022 additions and 1207 deletions
-2
View File
@@ -27,7 +27,6 @@ import sys
import getopt import getopt
import logging import logging
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QErrorMessage from PyQt5.QtWidgets import QApplication, QErrorMessage
from novelwriter.error import exceptionHandler, logException from novelwriter.error import exceptionHandler, logException
@@ -249,7 +248,6 @@ def main(sysArgs=None):
nwApp = QApplication([CONFIG.appName, (f"-style={qtStyle}")]) nwApp = QApplication([CONFIG.appName, (f"-style={qtStyle}")])
nwApp.setApplicationName(CONFIG.appName) nwApp.setApplicationName(CONFIG.appName)
nwApp.setApplicationVersion(__version__) nwApp.setApplicationVersion(__version__)
nwApp.setWindowIcon(QIcon(CONFIG.appIcon))
nwApp.setOrganizationDomain(__domain__) nwApp.setOrganizationDomain(__domain__)
# Connect the exception handler before making the main GUI # Connect the exception handler before making the main GUI
+5 -28
View File
@@ -23,12 +23,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import json import json
import uuid import uuid
import hashlib import hashlib
import logging import logging
from pathlib import Path
from datetime import datetime from datetime import datetime
from configparser import ConfigParser from configparser import ConfigParser
@@ -36,7 +36,7 @@ from PyQt5.QtCore import QCoreApplication
from PyQt5.QtWidgets import qApp from PyQt5.QtWidgets import qApp
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
from novelwriter.error import formatException, logException from novelwriter.error import logException
from novelwriter.constants import nwConst, nwUnicode from novelwriter.constants import nwConst, nwUnicode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -458,20 +458,16 @@ def jsonEncode(data, n=0, nmax=0):
def readTextFile(path): def readTextFile(path):
"""Read the content of a text file in a robust manner. """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 "" return ""
text = ""
try: try:
with open(path, mode="r", encoding="utf-8") as inFile: return path.read_text(encoding="utf-8")
text = inFile.read()
except Exception: except Exception:
logger.error("Could not read file: %s", path) logger.error("Could not read file: %s", path)
logException() logException()
return "" return ""
return text
def makeFileNameSafe(value): def makeFileNameSafe(value):
"""Returns a filename safe string of the value. """Returns a filename safe string of the value.
@@ -483,25 +479,6 @@ def makeFileNameSafe(value):
return clean 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): def sha256sum(path):
"""Make a shasum of a file using a buffer. """Make a shasum of a file using a buffer.
Based on: https://stackoverflow.com/a/44873382/5825851 Based on: https://stackoverflow.com/a/44873382/5825851
+256 -233
View File
@@ -23,12 +23,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import sys import sys
import json import json
import logging import logging
from time import time from time import time
from pathlib import Path
from PyQt5.Qt import PYQT_VERSION_STR from PyQt5.Qt import PYQT_VERSION_STR
from PyQt5.QtCore import ( from PyQt5.QtCore import (
@@ -37,7 +37,7 @@ from PyQt5.QtCore import (
) )
from novelwriter.error import logException, formatException 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 from novelwriter.constants import nwFiles, nwUnicode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -50,27 +50,51 @@ class Config:
def __init__(self): def __init__(self):
# Initialisation
# ==============
# Set Application Variables # Set Application Variables
self.appName = "novelWriter" self.appName = "novelWriter"
self.appHandle = "novelwriter" self.appHandle = "novelwriter"
# Set Paths # Set Paths
self.cmdOpen = None # Path from command line for project to be opened on launch confRoot = Path(QStandardPaths.writableLocation(QStandardPaths.ConfigLocation))
self.confPath = None # Folder where the config is saved dataRoot = Path(QStandardPaths.writableLocation(QStandardPaths.AppDataLocation))
self.dataPath = None # Folder where app data is stored
self.lastPath = None # The last user-selected folder (browse dialogs) self._confPath = confRoot.absolute() / self.appHandle # The user config location
self.appPath = None # The full path to the novelwriter package folder self._dataPath = dataRoot.absolute() / self.appHandle # The user data location
self.appRoot = None # The full path to the novelwriter root folder self._lastPath = Path.home().absolute() # The user's last used path
self.appIcon = None # The full path to the novelwriter icon file
self.assetPath = None # The full path to the novelwriter/assets folder self._appPath = Path(__file__).parent.absolute()
self.pdfDocs = None # The location of the PDF manual, if it exists 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
# Runtime Settings and Variables # Runtime Settings and Variables
self.hasError = False # True if the config class encountered an error self._hasError = False # True if the config class encountered an error
self.errData = [] # List of error messages self._errData = [] # List of error messages
self.confChanged = False # True whenever the config has chenged, false after save 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
# General # Localisation Info
self._qLocal = QLocale.system()
self._qtTrans = {}
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
# =============
self._recentProj = RecentProjects(self._dataPath)
# General GUI Settings
self.guiLang = self._qLocal.name()
self.guiTheme = "" # GUI theme self.guiTheme = "" # GUI theme
self.guiSyntax = "" # Syntax theme self.guiSyntax = "" # Syntax theme
self.guiFont = "" # Defaults to system default font self.guiFont = "" # Defaults to system default font
@@ -81,14 +105,7 @@ class Config:
self.setDefaultGuiTheme() self.setDefaultGuiTheme()
self.setDefaultSyntaxTheme() self.setDefaultSyntaxTheme()
# Localisation # Size Settings
self.qLocal = QLocale.system()
self.guiLang = self.qLocal.name()
self.qtLangPath = QLibraryInfo.location(QLibraryInfo.TranslationsPath)
self.nwLangPath = None
self.qtTrans = {}
# Sizes
self.winGeometry = [1200, 650] self.winGeometry = [1200, 650]
self.prefGeometry = [700, 615] self.prefGeometry = [700, 615]
self.projColWidth = [200, 60, 140] self.projColWidth = [200, 60, 140]
@@ -98,16 +115,16 @@ class Config:
self.outlnPanePos = [500, 150] self.outlnPanePos = [500, 150]
self.isFullScreen = False self.isFullScreen = False
# Features # Feature Settings
self.hideVScroll = False # Hide vertical scroll bars on main widgets self.hideVScroll = False # Hide vertical scroll bars on main widgets
self.hideHScroll = False # Hide horizontal 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 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.autoSaveProj = 60 # Interval for auto-saving project in seconds
self.autoSaveDoc = 30 # Interval for auto-saving document in seconds self.autoSaveDoc = 30 # Interval for auto-saving document in seconds
# Text Editor # Text Editor Settings
self.textFont = None # Editor font self.textFont = None # Editor font
self.textSize = 12 # Editor font size self.textSize = 12 # Editor font size
self.textWidth = 700 # Editor text width self.textWidth = 700 # Editor text width
@@ -146,7 +163,7 @@ class Config:
self.stopWhenIdle = True # Stop the status bar clock when the user is idle self.stopWhenIdle = True # Stop the status bar clock when the user is idle
self.userIdleTime = 300 # Time of inactivity to consider user idle self.userIdleTime = 300 # Time of inactivity to consider user idle
# User-Selected Symbols # User-Selected Symbol Settings
self.fmtApostrophe = nwUnicode.U_RSQUO self.fmtApostrophe = nwUnicode.U_RSQUO
self.fmtSingleQuotes = [nwUnicode.U_LSQUO, nwUnicode.U_RSQUO] self.fmtSingleQuotes = [nwUnicode.U_LSQUO, nwUnicode.U_RSQUO]
self.fmtDoubleQuotes = [nwUnicode.U_LDQUO, nwUnicode.U_RDQUO] self.fmtDoubleQuotes = [nwUnicode.U_LDQUO, nwUnicode.U_RDQUO]
@@ -154,8 +171,8 @@ class Config:
self.fmtPadAfter = "" self.fmtPadAfter = ""
self.fmtPadThin = False self.fmtPadThin = False
# Spell Checking # Spell Checking Settings
self.spellLanguage = None self.spellLanguage = "en"
# Search Bar Switches # Search Bar Switches
self.searchCase = False self.searchCase = False
@@ -165,8 +182,8 @@ class Config:
self.searchNextFile = False self.searchNextFile = False
self.searchMatchCap = False self.searchMatchCap = False
# Backup # Backup Settings
self.backupPath = "" self._backupPath = None
self.backupOnClose = False self.backupOnClose = False
self.askBeforeBackup = True self.askBeforeBackup = True
@@ -175,6 +192,9 @@ class Config:
self.viewComments = True # Comments are shown in the viewer self.viewComments = True # Comments are shown in the viewer
self.viewSynopsis = True # Synopsis is shown in the viewer self.viewSynopsis = True # Synopsis is shown in the viewer
# System and App Information
# ==========================
# Check Qt5 Versions # Check Qt5 Versions
verQt = splitVersionNumber(QT_VERSION_STR) verQt = splitVersionNumber(QT_VERSION_STR)
self.verQtString = QT_VERSION_STR self.verQtString = QT_VERSION_STR
@@ -226,6 +246,18 @@ class Config:
return return
##
# Properties
##
@property
def hasError(self):
return self._hasError
@property
def recentProjects(self):
return self._recentProj
## ##
# Methods # Methods
## ##
@@ -242,6 +274,44 @@ class Config:
""" """
return int(theSize/self.guiScale) return int(theSize/self.guiScale)
def dataPath(self, target=None):
"""Return a path in the data folder.
"""
if isinstance(target, str):
return self._dataPath / target
return self._dataPath
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 lastPath(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()
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.
"""
errMessage = "<br>".join(self._errData)
self._hasError = False
self._errData = []
return errMessage
## ##
# Config Actions # Config Actions
## ##
@@ -251,109 +321,64 @@ class Config:
and dataPath is mainly intended for the test suite. and dataPath is mainly intended for the test suite.
""" """
logger.debug("Initialising Config ...") logger.debug("Initialising Config ...")
if confPath is None: if isinstance(confPath, (str, Path)):
confRoot = QStandardPaths.writableLocation(QStandardPaths.ConfigLocation)
self.confPath = os.path.join(os.path.abspath(confRoot), self.appHandle)
else:
logger.info("Setting config from alternative path: %s", confPath) logger.info("Setting config from alternative path: %s", confPath)
self.confPath = confPath self._confPath = Path(confPath)
if isinstance(dataPath, (str, Path)):
if dataPath is None:
dataRoot = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)
self.dataPath = os.path.join(os.path.abspath(dataRoot), self.appHandle)
else:
logger.info("Setting data path from alternative path: %s", dataPath) 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("Config Path: %s", self._confPath)
logger.debug("Data path: %s", self.dataPath) logger.debug("Data Path: %s", self._dataPath)
logger.debug("App Root: %s", self._appRoot)
logger.debug("App Path: %s", self._appPath)
logger.debug("Last Path: %s", self._lastPath)
logger.debug("PDF Manual: %s", self.pdfDocs)
self.lastPath = os.path.expanduser("~") # If the config and data folders don't exist, create them
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
# This assumes that the os config and data folders exist # This assumes that the os config and data folders exist
if not ensureFolder(self.confPath, errLog=self.errData): self._confPath.mkdir(exist_ok=True)
self.hasError = True self._dataPath.mkdir(exist_ok=True)
self.confPath = None
if not ensureFolder(self.dataPath, errLog=self.errData): # Also create the syntax and themes folders if possible
self.hasError = True if self._dataPath.is_dir():
self.dataPath = None (self._dataPath / "syntax").mkdir(exist_ok=True)
(self._dataPath / "themes").mkdir(exist_ok=True)
# We don't error on these failing since they are not essential # Check if config file exists, and load it. If not, we save defaults
if self.dataPath is not None: if (self._confPath / nwFiles.CONF_FILE).is_file():
ensureFolder("syntax", parent=self.dataPath) self.loadConfig()
ensureFolder("themes", parent=self.dataPath) else:
self.saveConfig()
# Check if config file exists self._recentProj.loadCache()
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()
# Load recent projects cache
self.loadRecentCache()
# Check the availability of optional packages
self._checkOptionalPackages() self._checkOptionalPackages()
if not self.spellLanguage:
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)
self.pdfDocs = pdfDocs
logger.debug("Config initialisation complete") logger.debug("Config initialisation complete")
return True return
def initLocalisation(self, nwApp): def initLocalisation(self, nwApp):
"""Initialise the localisation of the GUI. """Initialise the localisation of the GUI.
""" """
self.qLocal = QLocale(self.guiLang) self._qLocal = QLocale(self.guiLang)
QLocale.setDefault(self.qLocal) QLocale.setDefault(self._qLocal)
self.qtTrans = {} self._qtTrans = {}
langList = [ langList = [
(self.qtLangPath, "qtbase"), # Qt 5.x (self._qtLangPath, "qtbase"), # Qt 5.x
(self.nwLangPath, "qtbase"), # Alternative Qt 5.x (self._nwLangPath, "qtbase"), # Alternative Qt 5.x
(self.nwLangPath, "nw"), # novelWriter (self._nwLangPath, "nw"), # novelWriter
] ]
for lngPath, lngBase in langList: for lngPath, lngBase in langList:
for lngCode in self.qLocal.uiLanguages(): for lngCode in self._qLocal.uiLanguages():
qTrans = QTranslator() qTrans = QTranslator()
lngFile = "%s_%s" % (lngBase, lngCode.replace("-", "_")) lngFile = "%s_%s" % (lngBase, lngCode.replace("-", "_"))
if lngFile not in self.qtTrans: 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)) logger.debug("Loaded: %s/%s", lngPath, lngFile)
nwApp.installTranslator(qTrans) nwApp.installTranslator(qTrans)
self.qtTrans[lngFile] = qTrans self._qtTrans[lngFile] = qTrans
return return
@@ -372,12 +397,12 @@ class Config:
else: else:
return [] return []
for qmFile in os.listdir(self.nwLangPath): for qmFile in Path(self._nwLangPath).iterdir():
if not os.path.isfile(os.path.join(self.nwLangPath, qmFile)): qmName = qmFile.name
if not (qmFile.is_file() and qmName.startswith(fPre) and qmName.endswith(fExt)):
continue continue
if not qmFile.startswith(fPre) or not qmFile.endswith(fExt):
continue qmLang = qmName[len(fPre):-len(fExt)]
qmLang = qmFile[len(fPre):-len(fExt)]
qmName = QLocale(qmLang).nativeLanguageName().title() qmName = QLocale(qmLang).nativeLanguageName().title()
if qmLang and qmName and qmLang != "en_GB": if qmLang and qmName and qmLang != "en_GB":
langList[qmLang] = qmName langList[qmLang] = qmName
@@ -388,20 +413,18 @@ class Config:
"""Load preferences from file and replace default settings. """Load preferences from file and replace default settings.
""" """
logger.debug("Loading config file") logger.debug("Loading config file")
if self.confPath is None:
return False
theConf = NWConfigParser() theConf = NWConfigParser()
cnfPath = os.path.join(self.confPath, nwFiles.CONF_FILE) cnfPath = self._confPath / nwFiles.CONF_FILE
try: try:
with open(cnfPath, mode="r", encoding="utf-8") as inFile: with open(cnfPath, mode="r", encoding="utf-8") as inFile:
theConf.read_file(inFile) theConf.read_file(inFile)
except Exception as exc: except Exception as exc:
logger.error("Could not load config file") logger.error("Could not load config file")
logException() logException()
self.hasError = True self._hasError = True
self.errData.append("Could not load config file") self._errData.append("Could not load config file")
self.errData.append(formatException(exc)) self._errData.append(formatException(exc))
return False return False
# Main # Main
@@ -473,9 +496,10 @@ class Config:
# Backup # Backup
cnfSec = "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.backupOnClose = theConf.rdBool(cnfSec, "backuponclose", self.backupOnClose)
self.askBeforeBackup = theConf.rdBool(cnfSec, "askbeforebackup", self.askBeforeBackup) self.askBeforeBackup = theConf.rdBool(cnfSec, "askbeforebackup", self.askBeforeBackup)
self.setBackupPath(backupPath)
# State # State
cnfSec = "State" cnfSec = "State"
@@ -491,7 +515,7 @@ class Config:
# Path # Path
cnfSec = "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 # Check Certain Values for None
self.spellLanguage = self._checkNone(self.spellLanguage) self.spellLanguage = self._checkNone(self.spellLanguage)
@@ -511,8 +535,6 @@ class Config:
"""Save the current preferences to file. """Save the current preferences to file.
""" """
logger.debug("Saving config file") logger.debug("Saving config file")
if self.confPath is None:
return False
theConf = NWConfigParser() theConf = NWConfigParser()
@@ -585,7 +607,7 @@ class Config:
} }
theConf["Backup"] = { theConf["Backup"] = {
"backuppath": str(self.backupPath), "backuppath": str(self._backupPath or ""),
"backuponclose": str(self.backupOnClose), "backuponclose": str(self.backupOnClose),
"askbeforebackup": str(self.askBeforeBackup), "askbeforebackup": str(self.askBeforeBackup),
} }
@@ -603,11 +625,11 @@ class Config:
} }
theConf["Path"] = { theConf["Path"] = {
"lastpath": str(self.lastPath), "lastpath": str(self._lastPath),
} }
# Write config file # Write config file
cnfPath = os.path.join(self.confPath, nwFiles.CONF_FILE) cnfPath = self._confPath / nwFiles.CONF_FILE
try: try:
with open(cnfPath, mode="w", encoding="utf-8") as outFile: with open(cnfPath, mode="w", encoding="utf-8") as outFile:
theConf.write(outFile) theConf.write(outFile)
@@ -615,102 +637,37 @@ class Config:
except Exception as exc: except Exception as exc:
logger.error("Could not save config file") logger.error("Could not save config file")
logException() logException()
self.hasError = True self._hasError = True
self.errData.append("Could not save config file") self._errData.append("Could not save config file")
self.errData.append(formatException(exc)) self._errData.append(formatException(exc))
return False return False
return True return True
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)
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.
"""
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+"~")
try:
with open(cacheTemp, mode="w+", encoding="utf-8") as outFile:
json.dump(self.recentProj, outFile, indent=2)
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):
"""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 # Setters
## ##
def setLastPath(self, lastPath): 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 lastPath is None or lastPath == "": if isinstance(lastPath, (str, Path)):
self.lastPath = "" lastPath = Path(lastPath)
else: if not lastPath.is_dir():
self.lastPath = os.path.dirname(lastPath) lastPath = lastPath.parent
return True if lastPath.is_dir():
self._lastPath = lastPath
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): def setWinSize(self, newWidth, newHeight):
"""Set the size of the main window, but only if the change is """Set the size of the main window, but only if the change is
@@ -726,7 +683,7 @@ class Config:
if abs(self.winGeometry[1] - newHeight) > 5: if abs(self.winGeometry[1] - newHeight) > 5:
self.winGeometry[1] = newHeight self.winGeometry[1] = newHeight
self.confChanged = True self.confChanged = True
return True return
def setPreferencesSize(self, newWidth, newHeight): def setPreferencesSize(self, newWidth, newHeight):
"""Sat the size of the Preferences dialog window. """Sat the size of the Preferences dialog window.
@@ -734,63 +691,63 @@ class Config:
self.prefGeometry[0] = int(newWidth/self.guiScale) self.prefGeometry[0] = int(newWidth/self.guiScale)
self.prefGeometry[1] = int(newHeight/self.guiScale) self.prefGeometry[1] = int(newHeight/self.guiScale)
self.confChanged = True self.confChanged = True
return True return
def setProjColWidths(self, colWidths): def setProjColWidths(self, colWidths):
"""Set the column widths of the Load Project dialog. """Set the column widths of the Load Project dialog.
""" """
self.projColWidth = [int(x/self.guiScale) for x in colWidths] self.projColWidth = [int(x/self.guiScale) for x in colWidths]
self.confChanged = True self.confChanged = True
return True return
def setMainPanePos(self, panePos): def setMainPanePos(self, panePos):
"""Set the position of the main GUI splitter. """Set the position of the main GUI splitter.
""" """
self.mainPanePos = [int(x/self.guiScale) for x in panePos] self.mainPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True self.confChanged = True
return True return
def setDocPanePos(self, panePos): def setDocPanePos(self, panePos):
"""Set the position of the main editor/viewer splitter. """Set the position of the main editor/viewer splitter.
""" """
self.docPanePos = [int(x/self.guiScale) for x in panePos] self.docPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True self.confChanged = True
return True return
def setViewPanePos(self, panePos): def setViewPanePos(self, panePos):
"""Set the position of the viewer meta data splitter. """Set the position of the viewer meta data splitter.
""" """
self.viewPanePos = [int(x/self.guiScale) for x in panePos] self.viewPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True self.confChanged = True
return True return
def setOutlinePanePos(self, panePos): def setOutlinePanePos(self, panePos):
"""Set the position of the outline details splitter. """Set the position of the outline details splitter.
""" """
self.outlnPanePos = [int(x/self.guiScale) for x in panePos] self.outlnPanePos = [int(x/self.guiScale) for x in panePos]
self.confChanged = True self.confChanged = True
return True return
def setShowRefPanel(self, checkState): def setShowRefPanel(self, checkState):
"""Set the visibility state of the reference panel. """Set the visibility state of the reference panel.
""" """
self.showRefPanel = checkState self.showRefPanel = checkState
self.confChanged = True self.confChanged = True
return self.showRefPanel return
def setViewComments(self, viewState): def setViewComments(self, viewState):
"""Set the visibility state of comments in the viewer. """Set the visibility state of comments in the viewer.
""" """
self.viewComments = viewState self.viewComments = viewState
self.confChanged = True self.confChanged = True
return self.viewComments return
def setViewSynopsis(self, viewState): def setViewSynopsis(self, viewState):
"""Set the visibility state of synopsis comments in the viewer. """Set the visibility state of synopsis comments in the viewer.
""" """
self.viewSynopsis = viewState self.viewSynopsis = viewState
self.confChanged = True self.confChanged = True
return self.viewSynopsis return
## ##
# Default Setters # Default Setters
@@ -843,15 +800,6 @@ class Config:
def getTabWidth(self): def getTabWidth(self):
return self.pxInt(max(self.tabWidth, 0)) 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 = "<br>".join(self.errData)
self.hasError = False
self.errData = []
return errMessage
## ##
# Internal Functions # Internal Functions
## ##
@@ -887,3 +835,78 @@ class Config:
return return
# END Class Config # 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
+2 -3
View File
@@ -24,7 +24,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import shutil import shutil
import logging import logging
import novelwriter import novelwriter
@@ -431,8 +430,8 @@ class ProjectBuilder:
logger.error("No project path set for the example project") logger.error("No project path set for the example project")
return False return False
pkgSample = os.path.join(self.mainConf.assetPath, "sample.zip") pkgSample = self.mainConf.assetPath("sample.zip")
if os.path.isfile(pkgSample): if pkgSample.is_file():
try: try:
shutil.unpack_archive(pkgSample, projPath) shutil.unpack_archive(pkgSample, projPath)
except Exception as exc: except Exception as exc:
+39 -34
View File
@@ -23,8 +23,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations
import json import json
import logging import logging
import novelwriter import novelwriter
@@ -49,7 +47,6 @@ from novelwriter.common import (
checkStringNone, formatTimeStamp, hexToInt, isHandle, makeFileNameSafe, minmax checkStringNone, formatTimeStamp, hexToInt, isHandle, makeFileNameSafe, minmax
) )
logger = logging.getLogger(__name__) 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._projOpened = 0 # The time stamp of when the project file was opened
self._projChanged = False # The project has unsaved changes self._projChanged = False # The project has unsaved changes
self._projAltered = False # The project has been altered this session self._projAltered = False # The project has been altered this session
self.lockedBy = None # Data on which computer has the project open self._lockedBy = None # Data on which computer has the project open
self._projFiles = [] # A list of all files in the content folder on load
# Class Settings
self.projFiles = [] # A list of all files in the content folder on load
# Internal Mapping # Internal Mapping
self.tr = partial(QCoreApplication.translate, "NWProject") self.tr = partial(QCoreApplication.translate, "NWProject")
@@ -127,6 +122,10 @@ class NWProject(QObject):
def projAltered(self): def projAltered(self):
return self._projAltered return self._projAltered
@property
def projFiles(self):
return self._projFiles
## ##
# Item Methods # Item Methods
## ##
@@ -246,7 +245,7 @@ class NWProject(QObject):
self._data = NWProjectData(self) self._data = NWProjectData(self)
# Project Settings # Project Settings
self.projFiles = [] self._projFiles = []
return return
@@ -274,7 +273,7 @@ class NWProject(QObject):
logger.warning("Failed to check lock file") logger.warning("Failed to check lock file")
else: else:
logger.error("Project is locked, so not opening") logger.error("Project is locked, so not opening")
self.lockedBy = lockStatus self._lockedBy = lockStatus
self.clearProject() self.clearProject()
return False return False
else: else:
@@ -355,10 +354,9 @@ class NWProject(QObject):
self._loadProjectLocalisation() self._loadProjectLocalisation()
# Update recent projects # Update recent projects
self.mainConf.updateRecentCache( self.mainConf.recentProjects.update(
self._storage.storagePath, self._data.name, sum(self._data.initCounts), time() self._storage.storagePath, self._data.name, sum(self._data.initCounts), time()
) )
self.mainConf.saveRecentCache()
# Check the project tree consistency # Check the project tree consistency
for tItem in self._tree: for tItem in self._tree:
@@ -425,10 +423,9 @@ class NWProject(QObject):
self._storage.runPostSaveTasks(autoSave=autoSave) self._storage.runPostSaveTasks(autoSave=autoSave)
# Update recent projects # Update recent projects
self.mainConf.updateRecentCache( self.mainConf.recentProjects.update(
self._storage.storagePath, self._data.name, sum(self._data.currCounts), saveTime self._storage.storagePath, self._data.name, sum(self._data.currCounts), saveTime
) )
self.mainConf.saveRecentCache()
self._storage.writeLockFile() self._storage.writeLockFile()
self.mainGui.setStatus(self.tr("Saved Project: {0}").format(self._data.name)) self.mainGui.setStatus(self.tr("Saved Project: {0}").format(self._data.name))
@@ -446,22 +443,9 @@ class NWProject(QObject):
self._storage.clearLockFile() self._storage.clearLockFile()
self._storage.closeSession() self._storage.closeSession()
self.clearProject() self.clearProject()
self.lockedBy = None self._lockedBy = None
return True 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): def backupProject(self, doNotify):
"""Create a zip file of the entire project. """Create a zip file of the entire project.
""" """
@@ -472,7 +456,8 @@ class NWProject(QObject):
logger.info("Backing up project") logger.info("Backing up project")
self.mainGui.setStatus(self.tr("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( self.mainGui.makeAlert(self.tr(
"Cannot backup project because no valid backup path is set. " "Cannot backup project because no valid backup path is set. "
"Please set a valid backup location in Preferences." "Please set a valid backup location in Preferences."
@@ -487,7 +472,7 @@ class NWProject(QObject):
return False return False
cleanName = makeFileNameSafe(self._data.name) cleanName = makeFileNameSafe(self._data.name)
baseDir = Path(self.mainConf.backupPath) / cleanName baseDir = backupPath / cleanName
try: try:
baseDir.mkdir(exist_ok=True) baseDir.mkdir(exist_ok=True)
except Exception as exc: except Exception as exc:
@@ -520,6 +505,19 @@ class NWProject(QObject):
# Setters # 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): def setProjectLang(self, theLang):
"""Set the project-specific language. """Set the project-specific language.
""" """
@@ -567,6 +565,13 @@ class NWProject(QObject):
# Getters # 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): def getFormattedAuthors(self):
"""Return a formatted string of authors. """Return a formatted string of authors.
""" """
@@ -692,13 +697,13 @@ class NWProject(QObject):
def _loadProjectLocalisation(self): def _loadProjectLocalisation(self):
"""Load the language data for the current project language. """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 = {} self._langData = {}
return False 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(): if not langFile.is_file():
langFile = Path(self.mainConf.nwLangPath) / "project_en_GB.json" langFile = Path(self.mainConf._nwLangPath) / "project_en_GB.json"
try: try:
with open(langFile, mode="r", encoding="utf-8") as inFile: with open(langFile, mode="r", encoding="utf-8") as inFile:
@@ -725,7 +730,7 @@ class NWProject(QObject):
# Then check the files in the data folder # Then check the files in the data folder
logger.debug("Checking files in project content folder") logger.debug("Checking files in project content folder")
orphanFiles = [] orphanFiles = []
self.projFiles = [] self._projFiles = []
for item in contentPath.iterdir(): for item in contentPath.iterdir():
itemName = item.name itemName = item.name
@@ -742,7 +747,7 @@ class NWProject(QObject):
continue continue
if fHandle in self._tree: if fHandle in self._tree:
self.projFiles.append(fHandle) self._projFiles.append(fHandle)
logger.debug("Checking file %s, handle '%s': OK", itemName, fHandle) logger.debug("Checking file %s, handle '%s': OK", itemName, fHandle)
else: else:
logger.warning("Checking file %s, handle '%s': Orphaned", itemName, fHandle) logger.warning("Checking file %s, handle '%s': Orphaned", itemName, fHandle)
+2 -3
View File
@@ -23,7 +23,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import logging import logging
import novelwriter import novelwriter
@@ -233,7 +232,7 @@ class GuiAbout(QDialog):
def _fillNotesPage(self): def _fillNotesPage(self):
"""Load the content for the Release Notes page. """Load the content for the Release Notes page.
""" """
docPath = os.path.join(self.mainConf.assetPath, "text", "release_notes.htm") docPath = self.mainConf.assetPath("text") / "release_notes.htm"
docText = readTextFile(docPath) docText = readTextFile(docPath)
if docText: if docText:
self.pageNotes.setHtml(docText) self.pageNotes.setHtml(docText)
@@ -244,7 +243,7 @@ class GuiAbout(QDialog):
def _fillLicensePage(self): def _fillLicensePage(self):
"""Load the content for the Licence page. """Load the content for the Licence page.
""" """
docPath = os.path.join(self.mainConf.assetPath, "text", "gplv3_en.htm") docPath = self.mainConf.assetPath("text") / "gplv3_en.htm"
docText = readTextFile(docPath) docText = readTextFile(docPath)
if docText: if docText:
self.pageLicense.setHtml(docText) self.pageLicense.setHtml(docText)
+4 -8
View File
@@ -23,7 +23,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import logging import logging
import novelwriter import novelwriter
@@ -384,7 +383,7 @@ class GuiPreferencesProjects(QWidget):
self.mainForm.addGroupLabel(self.tr("Project Backup")) self.mainForm.addGroupLabel(self.tr("Project Backup"))
# Backup Path # Backup Path
self.backupPath = self.mainConf.backupPath self.backupPath = self.mainConf.backupPath()
self.backupGetPath = QPushButton(self.tr("Browse")) self.backupGetPath = QPushButton(self.tr("Browse"))
self.backupGetPath.clicked.connect(self._backupFolder) self.backupGetPath.clicked.connect(self._backupFolder)
self.backupPathRow = self.mainForm.addRow( self.backupPathRow = self.mainForm.addRow(
@@ -451,7 +450,7 @@ class GuiPreferencesProjects(QWidget):
self.mainConf.autoSaveProj = self.autoSaveProj.value() self.mainConf.autoSaveProj = self.autoSaveProj.value()
# Project Backup # Project Backup
self.mainConf.backupPath = self.backupPath self.mainConf.setBackupPath(self.backupPath)
self.mainConf.backupOnClose = self.backupOnClose.isChecked() self.mainConf.backupOnClose = self.backupOnClose.isChecked()
self.mainConf.askBeforeBackup = self.askBeforeBackup.isChecked() self.mainConf.askBeforeBackup = self.askBeforeBackup.isChecked()
@@ -470,12 +469,9 @@ class GuiPreferencesProjects(QWidget):
def _backupFolder(self): def _backupFolder(self):
"""Open a dialog to select the backup folder. """Open a dialog to select the backup folder.
""" """
currDir = self.backupPath currDir = self.backupPath or ""
if not os.path.isdir(currDir):
currDir = ""
newDir = QFileDialog.getExistingDirectory( newDir = QFileDialog.getExistingDirectory(
self, self.tr("Backup Directory"), currDir, options=QFileDialog.ShowDirsOnly self, self.tr("Backup Directory"), str(currDir), options=QFileDialog.ShowDirsOnly
) )
if newDir: if newDir:
self.backupPath = newDir self.backupPath = newDir
+13 -19
View File
@@ -23,10 +23,10 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import logging import logging
import novelwriter import novelwriter
from pathlib import Path
from datetime import datetime from datetime import datetime
from PyQt5.QtGui import QKeySequence from PyQt5.QtGui import QKeySequence
@@ -190,8 +190,8 @@ class GuiProjectLoad(QDialog):
self, self.tr("Open Project"), "", filter=";;".join(extFilter) self, self.tr("Open Project"), "", filter=";;".join(extFilter)
) )
if projFile: if projFile:
thePath = os.path.abspath(os.path.dirname(projFile)) thePath = Path(projFile).absolute()
self.selPath.setText(thePath) self.selPath.setText(str(thePath))
self.openPath = thePath self.openPath = thePath
self.openState = self.OPEN_STATE self.openState = self.OPEN_STATE
self.accept() self.accept()
@@ -229,7 +229,7 @@ class GuiProjectLoad(QDialog):
).format(projName) ).format(projName)
) )
if msgYes: if msgYes:
self.mainConf.removeFromRecentCache( self.mainConf.recentProjects.remove(
selList[0].data(self.C_NAME, Qt.UserRole) selList[0].data(self.C_NAME, Qt.UserRole)
) )
self._populateList() self._populateList()
@@ -264,23 +264,17 @@ class GuiProjectLoad(QDialog):
def _populateList(self): def _populateList(self):
"""Populate the list box with recent project data. """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() self.listBox.clear()
sortList = sorted(dataList, key=lambda x: x[1], reverse=True) dataList = self.mainConf.recentProjects.listEntries()
for theTitle, theTime, theWords, projPath in sortList: 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 = QTreeWidgetItem([""]*4)
newItem.setIcon(self.C_NAME, self.mainGui.mainTheme.getIcon("proj_nwx")) newItem.setIcon(self.C_NAME, nwxIcon)
newItem.setText(self.C_NAME, theTitle) newItem.setText(self.C_NAME, title)
newItem.setData(self.C_NAME, Qt.UserRole, projPath) newItem.setData(self.C_NAME, Qt.UserRole, path)
newItem.setText(self.C_COUNT, formatInt(theWords)) newItem.setText(self.C_COUNT, formatInt(words))
newItem.setText(self.C_TIME, datetime.fromtimestamp(theTime).strftime("%x %X")) newItem.setText(self.C_TIME, datetime.fromtimestamp(time).strftime("%x %X"))
newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft | Qt.AlignVCenter) newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft | Qt.AlignVCenter)
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter) newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter)
newItem.setTextAlignment(self.C_TIME, Qt.AlignRight | Qt.AlignVCenter) newItem.setTextAlignment(self.C_TIME, Qt.AlignRight | Qt.AlignVCenter)
+8 -6
View File
@@ -26,6 +26,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging import logging
import novelwriter import novelwriter
from pathlib import Path
from urllib.parse import urljoin from urllib.parse import urljoin
from urllib.request import pathname2url from urllib.request import pathname2url
@@ -104,10 +105,11 @@ class GuiMainMenu(QMenuBar):
def _openUserManualFile(self): def _openUserManualFile(self):
"""Open the documentation in PDF format. """Open the documentation in PDF format.
""" """
if self.mainConf.pdfDocs is None: if isinstance(self.mainConf.pdfDocs, Path):
return False QDesktopServices.openUrl(
QDesktopServices.openUrl(QUrl(urljoin("file:", pathname2url(self.mainConf.pdfDocs)))) QUrl(urljoin("file:", pathname2url(str(self.mainConf.pdfDocs))))
return True )
return
## ##
# Menu Builders # Menu Builders
@@ -824,7 +826,7 @@ class GuiMainMenu(QMenuBar):
# Tools > Backup # Tools > Backup
self.aBackupProject = QAction(self.tr("Backup Project"), self) 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) self.toolsMenu.addAction(self.aBackupProject)
# Tools > Export Project # Tools > Export Project
@@ -881,7 +883,7 @@ class GuiMainMenu(QMenuBar):
self.helpMenu.addAction(self.aHelpDocs) self.helpMenu.addAction(self.aHelpDocs)
# Help > User Manual (PDF) # 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 = QAction(self.tr("User Manual (PDF)"), self)
self.aPdfDocs.setShortcut("Shift+F1") self.aPdfDocs.setShortcut("Shift+F1")
self.aPdfDocs.triggered.connect(self._openUserManualFile) self.aPdfDocs.triggered.connect(self._openUserManualFile)
+21 -28
View File
@@ -24,7 +24,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import logging import logging
import novelwriter import novelwriter
@@ -119,12 +118,10 @@ class GuiTheme:
self._availThemes = {} self._availThemes = {}
self._availSyntax = {} self._availSyntax = {}
self._listConf(self._availSyntax, os.path.join(self.mainConf.assetPath, "syntax")) self._listConf(self._availSyntax, self.mainConf.assetPath("syntax"))
self._listConf(self._availThemes, os.path.join(self.mainConf.assetPath, "themes")) self._listConf(self._availThemes, self.mainConf.assetPath("themes"))
self._listConf(self._availSyntax, self.mainConf.dataPath("syntax"))
if self.mainConf.dataPath: # Not guaranteed to be set self._listConf(self._availThemes, self.mainConf.dataPath("themes"))
self._listConf(self._availSyntax, os.path.join(self.mainConf.dataPath, "syntax"))
self._listConf(self._availThemes, os.path.join(self.mainConf.dataPath, "themes"))
self.loadTheme() self.loadTheme()
self.loadSyntax() self.loadSyntax()
@@ -380,13 +377,12 @@ class GuiTheme:
def _listConf(self, targetDict, checkDir): def _listConf(self, targetDict, checkDir):
"""Scan for theme config files and populate the dictionary. """Scan for theme config files and populate the dictionary.
""" """
if not os.path.isdir(checkDir): if not checkDir.is_dir():
return False return False
for checkFile in os.listdir(checkDir): for checkFile in checkDir.iterdir():
confPath = os.path.join(checkDir, checkFile) if checkFile.is_file() and checkFile.name.endswith(".conf"):
if os.path.isfile(confPath) and confPath.endswith(".conf"): targetDict[checkFile.name[:-5]] = checkFile
targetDict[checkFile[:-5]] = confPath
return True return True
@@ -476,7 +472,7 @@ class GuiIcons:
self._confName = "icons.conf" self._confName = "icons.conf"
# Icon Theme Path # Icon Theme Path
self._iconPath = os.path.join(self.mainConf.assetPath, "icons") self._iconPath = self.mainConf.assetPath("icons")
# Icon Theme Meta # Icon Theme Meta
self.themeName = "" self.themeName = ""
@@ -499,12 +495,12 @@ class GuiIcons:
update functions for the classes where they're used. update functions for the classes where they're used.
""" """
self._themeMap = {} self._themeMap = {}
themePath = os.path.join(self.mainConf.assetPath, "icons", iconTheme) themePath = self._iconPath / iconTheme
if not os.path.isdir(themePath): if not themePath.is_dir():
logger.warning("No icons loaded for '%s'", iconTheme) logger.warning("No icons loaded for '%s'", iconTheme)
return False return False
themeConf = os.path.join(themePath, self._confName) themeConf = themePath / self._confName
logger.info("Loading icon theme '%s'", iconTheme) logger.info("Loading icon theme '%s'", iconTheme)
# Config File # Config File
@@ -535,8 +531,8 @@ class GuiIcons:
if iconName not in self.ICON_KEYS: if iconName not in self.ICON_KEYS:
logger.error("Unknown icon name '%s' in config file", iconName) logger.error("Unknown icon name '%s' in config file", iconName)
else: else:
iconPath = os.path.join(themePath, iconFile) iconPath = themePath / iconFile
if os.path.isfile(iconPath): if iconPath.is_file():
self._themeMap[iconName] = iconPath self._themeMap[iconName] = iconPath
logger.debug("Icon slot '%s' using file '%s'", iconName, iconFile) logger.debug("Icon slot '%s' using file '%s'", iconName, iconFile)
else: else:
@@ -572,18 +568,16 @@ class GuiIcons:
if decoKey in self._themeMap: if decoKey in self._themeMap:
imgPath = self._themeMap[decoKey] imgPath = self._themeMap[decoKey]
elif decoKey in self.IMAGE_MAP: elif decoKey in self.IMAGE_MAP:
imgPath = os.path.join( imgPath = self.mainConf.assetPath("images") / self.IMAGE_MAP[decoKey]
self.mainConf.assetPath, "images", self.IMAGE_MAP[decoKey]
)
else: else:
logger.error("Decoration with name '%s' does not exist", decoKey) logger.error("Decoration with name '%s' does not exist", decoKey)
return QPixmap() return QPixmap()
if not os.path.isfile(imgPath): if not imgPath.is_file():
logger.error("Asset not found: %s", imgPath) logger.error("Asset not found: %s", imgPath)
return QPixmap() return QPixmap()
theDeco = QPixmap(imgPath) theDeco = QPixmap(str(imgPath))
if pxW is not None and pxH is not None: if pxW is not None and pxH is not None:
return theDeco.scaled(pxW, pxH, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) return theDeco.scaled(pxW, pxH, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
elif pxW is None and pxH is not None: 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 we just want the app icons, return right away
if iconKey == "novelwriter": if iconKey == "novelwriter":
return QIcon(os.path.join(self._iconPath, "novelwriter.svg")) return QIcon(str(self._iconPath / "novelwriter.svg"))
elif iconKey == "proj_nwx": 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 # Otherwise, we load from the theme folder
if iconKey in self._themeMap: if iconKey in self._themeMap:
relPath = os.path.relpath(self._themeMap[iconKey], self._iconPath) logger.debug("Loading: %s", self._themeMap[iconKey].name)
logger.debug("Loading: %s", relPath) return QIcon(str(self._themeMap[iconKey]))
return QIcon(self._themeMap[iconKey])
# If we didn't find one, give up and return an empty icon # If we didn't find one, give up and return an empty icon
logger.warning("Did not load an icon for '%s'", iconKey) logger.warning("Did not load an icon for '%s'", iconKey)
+16 -13
View File
@@ -23,12 +23,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import logging import logging
import novelwriter import novelwriter
from enum import Enum from enum import Enum
from time import time from time import time
from pathlib import Path
from datetime import datetime from datetime import datetime
from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot
@@ -95,7 +95,11 @@ class GuiMain(QMainWindow):
# Prepare Main Window # Prepare Main Window
self.resize(*self.mainConf.getWinSize()) self.resize(*self.mainConf.getWinSize())
self._updateWindowTitle() self._updateWindowTitle()
self.setWindowIcon(QIcon(self.mainConf.appIcon))
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)
# Build the GUI # Build the GUI
# ============= # =============
@@ -357,7 +361,7 @@ class GuiMain(QMainWindow):
logger.error("No projData or projPath set") logger.error("No projData or projPath set")
return False 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( self.makeAlert(self.tr(
"A project already exists in that location. " "A project already exists in that location. "
"Please choose another folder." "Please choose another folder."
@@ -409,7 +413,7 @@ class GuiMain(QMainWindow):
if not msgYes: if not msgYes:
doBackup = False doBackup = False
if doBackup: if doBackup:
self.theProject.backupProject(doNotify=False) self.theProject.backupProject(False)
else: else:
saveOK = True saveOK = True
@@ -447,7 +451,8 @@ class GuiMain(QMainWindow):
if not self.theProject.openProject(projFile): if not self.theProject.openProject(projFile):
# The project open failed. # 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 # The project is not locked, so failed for some other
# reason handled by the project class. # reason handled by the project class.
return False return False
@@ -459,10 +464,8 @@ class GuiMain(QMainWindow):
"'{0}' ({1} {2}), last active on {3}." "'{0}' ({1} {2}), last active on {3}."
) )
).format( ).format(
self.theProject.lockedBy[0], lockStatus[0], lockStatus[1], lockStatus[2],
self.theProject.lockedBy[1], datetime.fromtimestamp(int(lockStatus[3])).strftime("%x %X")
self.theProject.lockedBy[2],
datetime.fromtimestamp(int(self.theProject.lockedBy[3])).strftime("%x %X")
) )
except Exception: except Exception:
lockDetails = "" lockDetails = ""
@@ -694,7 +697,7 @@ class GuiMain(QMainWindow):
logger.error("No project open") logger.error("No project open")
return False return False
lastPath = self.mainConf.lastPath lastPath = self.mainConf.lastPath()
extFilter = [ extFilter = [
self.tr("Text files ({0})").format("*.txt"), self.tr("Text files ({0})").format("*.txt"),
self.tr("Markdown files ({0})").format("*.md"), self.tr("Markdown files ({0})").format("*.md"),
@@ -702,7 +705,7 @@ class GuiMain(QMainWindow):
self.tr("All files ({0})").format("*"), self.tr("All files ({0})").format("*"),
] ]
loadFile, _ = QFileDialog.getOpenFileName( 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: if not loadFile:
return False return False
@@ -1140,7 +1143,7 @@ class GuiMain(QMainWindow):
errors since it is initialised before the GUI itself. errors since it is initialised before the GUI itself.
""" """
if self.mainConf.hasError: if self.mainConf.hasError:
self.makeAlert(self.mainConf.getErrData(), nwAlert.ERROR) self.makeAlert(self.mainConf.errorText(), nwAlert.ERROR)
return True return True
return False return False
@@ -1363,7 +1366,7 @@ class GuiMain(QMainWindow):
# Help # Help
self.addAction(self.mainMenu.aHelpDocs) self.addAction(self.mainMenu.aHelpDocs)
if self.mainConf.pdfDocs is not None: if isinstance(self.mainConf.pdfDocs, Path):
self.addAction(self.mainMenu.aPdfDocs) self.addAction(self.mainMenu.aPdfDocs)
return True return True
+2 -7
View File
@@ -23,7 +23,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import json import json
import logging import logging
import novelwriter import novelwriter
@@ -891,13 +890,9 @@ class GuiBuildNovel(QDialog):
cleanName = makeFileNameSafe(self.theProject.data.name) cleanName = makeFileNameSafe(self.theProject.data.name)
fileName = "%s.%s" % (cleanName, fileExt) fileName = "%s.%s" % (cleanName, fileExt)
saveDir = self.mainConf.lastPath savePath = self.mainConf.lastPath() / fileName
if not os.path.isdir(saveDir):
saveDir = os.path.expanduser("~")
savePath = os.path.join(saveDir, fileName)
savePath, _ = QFileDialog.getSaveFileName( savePath, _ = QFileDialog.getSaveFileName(
self, self.tr("Save Document As"), savePath self, self.tr("Save Document As"), str(savePath)
) )
if not savePath: if not savePath:
return False return False
+1 -2
View File
@@ -23,7 +23,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import random import random
import logging import logging
import novelwriter import novelwriter
@@ -120,7 +119,7 @@ class GuiLipsum(QDialog):
def _doInsert(self): def _doInsert(self):
"""Load the text and insert it in the open document. """Load the text and insert it in the open document.
""" """
lipsumFile = os.path.join(self.mainConf.assetPath, "text", "lipsum.txt") lipsumFile = self.mainConf.assetPath("text") / "lipsum.txt"
lipsumText = readTextFile(lipsumFile).splitlines() lipsumText = readTextFile(lipsumFile).splitlines()
if self.randSwitch.isChecked(): if self.randSwitch.isChecked():
+2 -5
View File
@@ -236,12 +236,9 @@ class ProjWizardFolderPage(QWizardPage):
def _doBrowse(self): def _doBrowse(self):
"""Select a project folder. """Select a project folder.
""" """
lastPath = self.mainConf.lastPath lastPath = self.mainConf.lastPath()
if not os.path.isdir(lastPath):
lastPath = ""
projDir = QFileDialog.getExistingDirectory( 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: if projDir:
projName = self.field("projName") projName = self.field("projName")
+2 -9
View File
@@ -23,7 +23,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import json import json
import logging import logging
import novelwriter import novelwriter
@@ -363,15 +362,9 @@ class GuiWritingStats(QDialog):
return False return False
# Generate the file name # Generate the file name
saveDir = self.mainConf.lastPath savePath = self.mainConf.lastPath() / f"sessionStats.{fileExt}"
if not os.path.isdir(saveDir):
saveDir = os.path.expanduser("~")
fileName = "sessionStats.%s" % fileExt
savePath = os.path.join(saveDir, fileName)
savePath, _ = QFileDialog.getSaveFileName( 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: if not savePath:
return False return False
+53 -85
View File
@@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import sys import sys
import pytest import pytest
import shutil import shutil
@@ -50,25 +49,14 @@ def initQt(qtbot):
## ##
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def tmpDir(): def tmpPath():
"""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):
"""A temporary folder for the test session. Path version. """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") @pytest.fixture(scope="session")
@@ -99,57 +87,15 @@ def fncPath(tmpPath):
return fncPath 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") @pytest.fixture(scope="function")
def fncDir(tmpDir): def projPath(fncPath):
"""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):
"""A temporary folder for a single test function, """A temporary folder for a single test function,
with a project folder. with a project folder.
""" """
prjDir = os.path.join(fncDir, "project") prjDir = fncPath / "project"
if os.path.isdir(prjDir): if prjDir.exists():
shutil.rmtree(prjDir) shutil.rmtree(prjDir)
if not os.path.isdir(prjDir): prjDir.mkdir(exist_ok=True)
os.mkdir(prjDir)
return prjDir return prjDir
@@ -158,29 +104,29 @@ def fncProj(fncDir):
## ##
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def tmpConf(tmpDir): def tmpConf(tmpPath):
"""Create a temporary novelWriter configuration object. """Create a temporary novelWriter configuration object.
""" """
confFile = os.path.join(tmpDir, "novelwriter.conf") confFile = tmpPath / "novelwriter.conf"
if os.path.isfile(confFile): if confFile.is_file():
os.unlink(confFile) confFile.unlink()
theConf = Config() theConf = Config()
theConf.initConfig(tmpDir, tmpDir) theConf.initConfig(tmpPath, tmpPath)
theConf.setLastPath("") theConf.setLastPath(tmpPath)
theConf.guiLang = "en_GB" theConf.guiLang = "en_GB"
return theConf return theConf
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def fncConf(fncDir): def fncConf(fncPath):
"""Create a temporary novelWriter configuration object. """Create a temporary novelWriter configuration object.
""" """
confFile = os.path.join(fncDir, "novelwriter.conf") confFile = fncPath / "novelwriter.conf"
if os.path.isfile(confFile): if confFile.is_file():
os.unlink(confFile) confFile.unlink()
theConf = Config() theConf = Config()
theConf.initConfig(fncDir, fncDir) theConf.initConfig(fncPath, fncPath)
theConf.setLastPath("") theConf.setLastPath(fncPath)
theConf.guiLang = "en_GB" theConf.guiLang = "en_GB"
return theConf return theConf
@@ -196,7 +142,7 @@ def mockGUI(monkeypatch, tmpConf):
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def nwGUI(qtbot, monkeypatch, fncDir, fncConf): def nwGUI(qtbot, monkeypatch, fncPath, fncConf):
"""Create an instance of the novelWriter GUI. """Create an instance of the novelWriter GUI.
""" """
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Ok) monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Ok)
@@ -205,12 +151,12 @@ def nwGUI(qtbot, monkeypatch, fncDir, fncConf):
monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr("novelwriter.CONFIG", fncConf) 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) qtbot.addWidget(nwGUI)
nwGUI.show() nwGUI.show()
qtbot.wait(20) qtbot.wait(20)
nwGUI.mainConf.lastPath = fncDir nwGUI.mainConf.setLastPath(fncPath)
yield nwGUI yield nwGUI
@@ -252,14 +198,36 @@ def mockRnd(monkeypatch):
## ##
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def nwLipsum(tmpDir): def nwLipsum(tmpPath):
"""A medium sized novelWriter example project with a lot of Lorem """A medium sized novelWriter example project with a lot of Lorem
Ipsum text. Ipsum text.
""" """
tstDir = os.path.dirname(__file__) tstDir = Path(__file__).parent
srcDir = os.path.join(tstDir, "lipsum") srcDir = tstDir / "lipsum"
dstDir = os.path.join(tmpDir, "lipsum") dstDir = tmpPath / "lipsum"
if os.path.isdir(dstDir): 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.rmtree(dstDir)
shutil.copytree(srcDir, dstDir) shutil.copytree(srcDir, dstDir)
@@ -267,7 +235,7 @@ def nwLipsum(tmpDir):
yield dstDir yield dstDir
if os.path.isdir(dstDir): if dstDir.exists():
shutil.rmtree(dstDir) shutil.rmtree(dstDir)
return return
+13 -40
View File
@@ -19,10 +19,9 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import hashlib
import os
import time import time
import pytest import pytest
import hashlib
from mock import causeOSError from mock import causeOSError
from tools import writeFile from tools import writeFile
@@ -33,8 +32,8 @@ from novelwriter.common import (
checkUuid, isHandle, isTitleTag, isItemClass, isItemType, isItemLayout, checkUuid, isHandle, isTitleTag, isItemClass, isItemType, isItemLayout,
hexToInt, minmax, checkIntTuple, formatInt, formatTimeStamp, formatTime, hexToInt, minmax, checkIntTuple, formatInt, formatTimeStamp, formatTime,
simplified, yesNo, splitVersionNumber, transferCase, fuzzyTime, simplified, yesNo, splitVersionNumber, transferCase, fuzzyTime,
numberToRoman, jsonEncode, readTextFile, makeFileNameSafe, ensureFolder, numberToRoman, jsonEncode, readTextFile, makeFileNameSafe, sha256sum,
sha256sum, getGuiItem, NWConfigParser getGuiItem, NWConfigParser
) )
@@ -591,18 +590,18 @@ def testBaseCommon_JsonEncode():
@pytest.mark.base @pytest.mark.base
def testBaseCommon_ReadTextFile(monkeypatch, fncDir, ipsumText): def testBaseCommon_ReadTextFile(monkeypatch, fncPath, ipsumText):
"""Test the readTextFile function. """Test the readTextFile function.
""" """
testText = "\n\n".join(ipsumText) + "\n" testText = "\n\n".join(ipsumText) + "\n"
testFile = os.path.join(fncDir, "ipsum.txt") testFile = fncPath / "ipsum.txt"
writeFile(testFile, testText) writeFile(testFile, testText)
assert readTextFile(os.path.join(fncDir, "not_a_file.txt")) == "" assert readTextFile(fncPath / "not_a_file.txt") == ""
assert readTextFile(testFile) == testText assert readTextFile(testFile) == testText
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError) mp.setattr("pathlib.Path.read_text", causeOSError)
assert readTextFile(testFile) == "" assert readTextFile(testFile) == ""
# END Test testBaseCommon_ReadTextFile # END Test testBaseCommon_ReadTextFile
@@ -621,33 +620,7 @@ def testBaseCommon_MakeFileNameSafe():
@pytest.mark.base @pytest.mark.base
def testBaseCommon_EnsureFolder(monkeypatch, fncDir): def testBaseCommon_Sha256Sum(monkeypatch, fncPath, ipsumText):
"""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):
"""Test the sha256sum function. """Test the sha256sum function.
""" """
longText = 50*(" ".join(ipsumText) + " ") longText = 50*(" ".join(ipsumText) + " ")
@@ -656,9 +629,9 @@ def testBaseCommon_Sha256Sum(monkeypatch, fncDir, ipsumText):
assert len(longText) == 175650 assert len(longText) == 175650
longFile = os.path.join(fncDir, "long_file.txt") longFile = fncPath / "long_file.txt"
shortFile = os.path.join(fncDir, "short_file.txt") shortFile = fncPath / "short_file.txt"
noneFile = os.path.join(fncDir, "none_file.txt") noneFile = fncPath / "none_file.txt"
writeFile(longFile, longText) writeFile(longFile, longText)
writeFile(shortFile, shortText) writeFile(shortFile, shortText)
@@ -697,10 +670,10 @@ def testBaseCommon_GetGuiItem(nwGUI):
@pytest.mark.base @pytest.mark.base
def testBaseCommon_NWConfigParser(fncDir): def testBaseCommon_NWConfigParser(fncPath):
"""Test the NWConfigParser subclass. """Test the NWConfigParser subclass.
""" """
tstConf = os.path.join(fncDir, "test.cfg") tstConf = fncPath / "test.cfg"
writeFile(tstConf, ( writeFile(tstConf, (
"[main]\n" "[main]\n"
"stropt = value\n" "stropt = value\n"
+219 -226
View File
@@ -19,16 +19,16 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import sys import sys
import pytest import pytest
from shutil import copyfile from shutil import copyfile
from pathlib import Path
from mock import causeOSError, MockApp from mock import causeOSError, MockApp
from tools import cmpFiles, writeFile from tools import cmpFiles, writeFile
from novelwriter.config import Config from novelwriter.config import Config, RecentProjects
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
@@ -37,172 +37,140 @@ def testBaseConfig_Constructor(monkeypatch):
"""Test config contructor. """Test config contructor.
""" """
# Linux # Linux
monkeypatch.setattr("sys.platform", "linux") with monkeypatch.context() as mp:
tstConf = Config() mp.setattr("sys.platform", "linux")
assert tstConf.osLinux is True tstConf = Config()
assert tstConf.osDarwin is False assert tstConf.osLinux is True
assert tstConf.osWindows is False assert tstConf.osDarwin is False
assert tstConf.osUnknown is False assert tstConf.osWindows is False
assert tstConf.osUnknown is False
# macOS # macOS
monkeypatch.setattr("sys.platform", "darwin") with monkeypatch.context() as mp:
tstConf = Config() mp.setattr("sys.platform", "darwin")
assert tstConf.osLinux is False tstConf = Config()
assert tstConf.osDarwin is True assert tstConf.osLinux is False
assert tstConf.osWindows is False assert tstConf.osDarwin is True
assert tstConf.osUnknown is False assert tstConf.osWindows is False
assert tstConf.osUnknown is False
# Windows # Windows
monkeypatch.setattr("sys.platform", "win32") with monkeypatch.context() as mp:
tstConf = Config() mp.setattr("sys.platform", "win32")
assert tstConf.osLinux is False tstConf = Config()
assert tstConf.osDarwin is False assert tstConf.osLinux is False
assert tstConf.osWindows is True assert tstConf.osDarwin is False
assert tstConf.osUnknown is False assert tstConf.osWindows is True
assert tstConf.osUnknown is False
# Cygwin # Cygwin
monkeypatch.setattr("sys.platform", "cygwin") with monkeypatch.context() as mp:
tstConf = Config() mp.setattr("sys.platform", "cygwin")
assert tstConf.osLinux is False tstConf = Config()
assert tstConf.osDarwin is False assert tstConf.osLinux is False
assert tstConf.osWindows is True assert tstConf.osDarwin is False
assert tstConf.osUnknown is False assert tstConf.osWindows is True
assert tstConf.osUnknown is False
# Other # Other
monkeypatch.setattr("sys.platform", "some_other_os") with monkeypatch.context() as mp:
tstConf = Config() mp.setattr("sys.platform", "some_other_os")
assert tstConf.osLinux is False tstConf = Config()
assert tstConf.osDarwin is False assert tstConf.osLinux is False
assert tstConf.osWindows is False assert tstConf.osDarwin is False
assert tstConf.osUnknown is True 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 # END Test testBaseConfig_Constructor
@pytest.mark.base @pytest.mark.base
def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): def testBaseConfig_InitLoadSave(monkeypatch, fncPath, tstPaths):
"""Test config intialisation. """Test config intialisation.
""" """
tstConf = Config() tstConf = Config()
confFile = os.path.join(tmpDir, "novelwriter.conf") confFile = fncPath / nwFiles.CONF_FILE
testFile = os.path.join(outDir, "baseConfig_novelwriter.conf") testFile = tstPaths.outDir / "baseConfig_novelwriter.conf"
compFile = os.path.join(refDir, "baseConfig_novelwriter.conf") compFile = tstPaths.refDir / "baseConfig_novelwriter.conf"
# Make sure we don't have any old conf file # Make sure we don't have any old conf file
if os.path.isfile(confFile): if confFile.is_file():
os.unlink(confFile) confFile.unlink()
# Let the config class figure out the path # Running init against a new oath should write a new config file
with monkeypatch.context() as mp: tstConf.initConfig(confPath=fncPath, dataPath=fncPath)
mp.setattr("PyQt5.QtCore.QStandardPaths.writableLocation", lambda *a: fncDir) assert tstConf._confPath == fncPath
tstConf.initConfig() assert tstConf._dataPath == fncPath
assert tstConf.confPath == os.path.join(fncDir, tstConf.appHandle) assert confFile.exists()
assert tstConf.dataPath == os.path.join(fncDir, tstConf.appHandle)
assert not os.path.isfile(confFile)
# Fail to make folders # Check that we have a default file
with monkeypatch.context() as mp: copyfile(confFile, testFile)
mp.setattr("os.mkdir", causeOSError) 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") # Block saving the file
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
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError) mp.setattr("builtins.open", causeOSError)
assert tstConf.saveConfig() is False
assert not tstConf.loadConfig()
assert tstConf.hasError is True assert tstConf.hasError is True
assert tstConf.errData != [] assert tstConf.errorText().startswith("Could not save config file")
assert tstConf.getErrData().startswith("Could not")
assert tstConf.hasError is False
assert tstConf.errData == []
assert not tstConf.saveConfig() # Block loading the file
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
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir) mp.setattr("builtins.open", causeOSError)
assert tstConf.confPath == tmpDir assert tstConf.loadConfig() is False
assert tstConf.dataPath == tmpDir assert tstConf.hasError is True
appRoot = tstConf.appRoot assert tstConf.errorText().startswith("Could not load config file")
mp.setattr("os.path.isfile", lambda *a: True) # Change a few settings, save, reset, and reload
tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir) tstConf.guiTheme = "foo"
assert tstConf.confPath == tmpDir tstConf.guiSyntax = "bar"
assert tstConf.dataPath == tmpDir
assert tstConf.appRoot == os.path.dirname(appRoot)
assert tstConf.appPath == os.path.dirname(appRoot)
assert tstConf.loadConfig() is True
assert tstConf.saveConfig() is True assert tstConf.saveConfig() is True
# Test Correcting Quote Settings newConf = Config()
origDbl = tstConf.fmtDoubleQuotes newConf.initConfig(confPath=fncPath, dataPath=fncPath)
origSng = tstConf.fmtSingleQuotes assert newConf.guiTheme == "foo"
orDoDbl = tstConf.doReplaceDQuote assert newConf.guiSyntax == "bar"
orDoSng = tstConf.doReplaceSQuote
# Test Correcting Quote Settings
tstConf.fmtDoubleQuotes = ["\"", "\""] tstConf.fmtDoubleQuotes = ["\"", "\""]
tstConf.fmtSingleQuotes = ["'", "'"] tstConf.fmtSingleQuotes = ["'", "'"]
tstConf.doReplaceDQuote = True tstConf.doReplaceDQuote = True
tstConf.doReplaceSQuote = True tstConf.doReplaceSQuote = True
assert tstConf.saveConfig() is True assert tstConf.saveConfig() is True
assert tstConf.loadConfig() is True assert newConf.loadConfig() is True
assert tstConf.doReplaceDQuote is False assert newConf.doReplaceDQuote is False
assert tstConf.doReplaceSQuote is False assert newConf.doReplaceSQuote is False
tstConf.fmtDoubleQuotes = origDbl # END Test testBaseConfig_InitLoadSave
tstConf.fmtSingleQuotes = origSng
tstConf.doReplaceDQuote = orDoDbl
tstConf.doReplaceSQuote = orDoSng @pytest.mark.base
assert tstConf.saveConfig() is True def testBaseConfig_Localisation(fncPath, tstPaths):
"""Test localisation.
"""
tstConf = Config()
tstConf.initConfig(confPath=fncPath, dataPath=fncPath)
# Localisation # Localisation
# ============ # ============
i18nDir = os.path.join(fncDir, "i18n") i18nDir = fncPath / "i18n"
os.mkdir(i18nDir) i18nDir.mkdir()
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")) copyfile(tstPaths.filesDir / "nw_en_GB.qm", i18nDir / "nw_en_GB.qm")
writeFile(os.path.join(i18nDir, "nw_en_GB.ts"), "") writeFile(i18nDir / "nw_en_GB.ts", "")
writeFile(os.path.join(i18nDir, "nw_abcd.qm"), "") writeFile(i18nDir / "nw_abcd.qm", "")
tstApp = MockApp() tstApp = MockApp()
tstConf.initLocalisation(tstApp) tstConf.initLocalisation(tstApp)
@@ -216,88 +184,55 @@ def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir):
assert theList == [] assert theList == []
# Add Language # Add Language
copyfile(os.path.join(filesDir, "nw_en_GB.qm"), os.path.join(i18nDir, "nw_fr.qm")) copyfile(tstPaths.filesDir / "nw_en_GB.qm", i18nDir / "nw_fr.qm")
writeFile(os.path.join(i18nDir, "nw_fr.ts"), "") writeFile(i18nDir / "nw_fr.ts", "")
theList = tstConf.listLanguages(tstConf.LANG_NW) theList = tstConf.listLanguages(tstConf.LANG_NW)
assert theList == [("en_GB", "British English"), ("fr", "Français")] assert theList == [("en_GB", "British English"), ("fr", "Français")]
copyfile(confFile, testFile) # END Test testBaseConfig_Localisation
assert cmpFiles(testFile, compFile, ignoreStart=("timestamp", "lastnotes", "guilang"))
# END Test testBaseConfig_Init
@pytest.mark.base @pytest.mark.base
def testBaseConfig_RecentCache(monkeypatch, tmpConf, tmpDir, fncDir): def testBaseConfig_Methods(tmpConf, tmpPath):
"""Test recent cache file. """Check class methods.
""" """
# Check failing # Data Path
tmpConf.dataPath = None assert tmpConf.dataPath() == tmpPath
assert not tmpConf.loadRecentCache() assert tmpConf.dataPath("stuff") == tmpPath / "stuff"
assert not tmpConf.saveRecentCache()
tmpConf.dataPath = tmpDir
# Add a couple of values # Assets Path
pathOne = os.path.join(fncDir, "projPathOne", nwFiles.PROJ_FILE) appPath = tmpConf._appPath
pathTwo = os.path.join(fncDir, "projPathTwo", nwFiles.PROJ_FILE) assert tmpConf.assetPath() == appPath / "assets"
assert tmpConf.updateRecentCache(pathOne, "Proj One", 100, 1600002000) assert tmpConf.assetPath("stuff") == appPath / "assets" / "stuff"
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},
}
# Fail to Save # Last Path
with monkeypatch.context() as mp: assert tmpConf.lastPath() == tmpPath
mp.setattr("builtins.open", causeOSError)
assert not tmpConf.saveRecentCache()
# Save Proper tmpStuff = tmpPath / "stuff"
cacheFile = os.path.join(tmpDir, nwFiles.RECENT_FILE) tmpStuff.mkdir()
assert tmpConf.saveRecentCache() tmpConf.setLastPath(tmpStuff)
assert tmpConf.saveRecentCache() assert tmpConf.lastPath() == tmpStuff
assert os.path.isfile(cacheFile)
# Fail to Load fileStuff = tmpStuff / "more_stuff.txt"
with monkeypatch.context() as mp: fileStuff.write_text("Stuff")
mp.setattr("builtins.open", causeOSError) tmpConf.setLastPath(fileStuff)
tmpConf.recentProj = {} assert tmpConf.lastPath() == tmpStuff
assert not tmpConf.loadRecentCache()
assert tmpConf.recentProj == {}
# Load Proper fileStuff.unlink()
tmpConf.recentProj = {} tmpStuff.rmdir()
assert tmpConf.loadRecentCache() assert tmpConf.lastPath() == Path.home().absolute()
assert tmpConf.recentProj == {
pathOne: {"time": 1600002000, "title": "Proj One", "words": 100},
pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200},
}
# Remove Non-Existent Entry # Recent Projects
assert not tmpConf.removeFromRecentCache("stuff") assert isinstance(tmpConf.recentProjects, RecentProjects)
assert tmpConf.recentProj == {
pathOne: {"time": 1600002000, "title": "Proj One", "words": 100},
pathTwo: {"time": 1600005600, "title": "Proj Two", "words": 200},
}
# Remove Second Entry # END Test testBaseConfig_Methods
assert tmpConf.removeFromRecentCache(pathTwo)
assert tmpConf.recentProj == {
pathOne: {"time": 1600002000, "title": "Proj One", "words": 100},
}
# END Test testBaseConfig_RecentCache
@pytest.mark.base @pytest.mark.base
def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir): def testBaseConfig_SettersGetters(tmpConf):
"""Set various sizes and positions """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 # GUI Scaling
# =========== # ===========
@@ -318,98 +253,98 @@ def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir):
# Window Size # Window Size
tmpConf.guiScale = 1.0 tmpConf.guiScale = 1.0
assert tmpConf.setWinSize(1205, 655) tmpConf.setWinSize(1205, 655)
assert not tmpConf.confChanged assert tmpConf.confChanged is False
tmpConf.guiScale = 2.0 tmpConf.guiScale = 2.0
assert tmpConf.setWinSize(70, 70) tmpConf.setWinSize(70, 70)
assert tmpConf.getWinSize() == [70, 70] assert tmpConf.getWinSize() == [70, 70]
assert tmpConf.winGeometry == [35, 35] assert tmpConf.winGeometry == [35, 35]
tmpConf.guiScale = 1.0 tmpConf.guiScale = 1.0
assert tmpConf.setWinSize(70, 70) tmpConf.setWinSize(70, 70)
assert tmpConf.getWinSize() == [70, 70] assert tmpConf.getWinSize() == [70, 70]
assert tmpConf.winGeometry == [70, 70] assert tmpConf.winGeometry == [70, 70]
assert tmpConf.setWinSize(1200, 650) tmpConf.setWinSize(1200, 650)
# Preferences Size # Preferences Size
tmpConf.guiScale = 2.0 tmpConf.guiScale = 2.0
assert tmpConf.setPreferencesSize(70, 70) tmpConf.setPreferencesSize(70, 70)
assert tmpConf.getPreferencesSize() == [70, 70] assert tmpConf.getPreferencesSize() == [70, 70]
assert tmpConf.prefGeometry == [35, 35] assert tmpConf.prefGeometry == [35, 35]
tmpConf.guiScale = 1.0 tmpConf.guiScale = 1.0
assert tmpConf.setPreferencesSize(70, 70) tmpConf.setPreferencesSize(70, 70)
assert tmpConf.getPreferencesSize() == [70, 70] assert tmpConf.getPreferencesSize() == [70, 70]
assert tmpConf.prefGeometry == [70, 70] assert tmpConf.prefGeometry == [70, 70]
assert tmpConf.setPreferencesSize(700, 615) tmpConf.setPreferencesSize(700, 615)
# Project Settings Tree Columns # Project Settings Tree Columns
tmpConf.guiScale = 2.0 tmpConf.guiScale = 2.0
assert tmpConf.setProjColWidths([10, 20, 30]) tmpConf.setProjColWidths([10, 20, 30])
assert tmpConf.getProjColWidths() == [10, 20, 30] assert tmpConf.getProjColWidths() == [10, 20, 30]
assert tmpConf.projColWidth == [5, 10, 15] assert tmpConf.projColWidth == [5, 10, 15]
tmpConf.guiScale = 1.0 tmpConf.guiScale = 1.0
assert tmpConf.setProjColWidths([10, 20, 30]) tmpConf.setProjColWidths([10, 20, 30])
assert tmpConf.getProjColWidths() == [10, 20, 30] assert tmpConf.getProjColWidths() == [10, 20, 30]
assert tmpConf.projColWidth == [10, 20, 30] assert tmpConf.projColWidth == [10, 20, 30]
assert tmpConf.setProjColWidths([200, 60, 140]) tmpConf.setProjColWidths([200, 60, 140])
# Main Pane Splitter # Main Pane Splitter
tmpConf.guiScale = 2.0 tmpConf.guiScale = 2.0
assert tmpConf.setMainPanePos([200, 700]) tmpConf.setMainPanePos([200, 700])
assert tmpConf.getMainPanePos() == [200, 700] assert tmpConf.getMainPanePos() == [200, 700]
assert tmpConf.mainPanePos == [100, 350] assert tmpConf.mainPanePos == [100, 350]
tmpConf.guiScale = 1.0 tmpConf.guiScale = 1.0
assert tmpConf.setMainPanePos([200, 700]) tmpConf.setMainPanePos([200, 700])
assert tmpConf.getMainPanePos() == [200, 700] assert tmpConf.getMainPanePos() == [200, 700]
assert tmpConf.mainPanePos == [200, 700] assert tmpConf.mainPanePos == [200, 700]
assert tmpConf.setMainPanePos([300, 800]) tmpConf.setMainPanePos([300, 800])
# Doc Pane Splitter # Doc Pane Splitter
tmpConf.guiScale = 2.0 tmpConf.guiScale = 2.0
assert tmpConf.setDocPanePos([300, 300]) tmpConf.setDocPanePos([300, 300])
assert tmpConf.getDocPanePos() == [300, 300] assert tmpConf.getDocPanePos() == [300, 300]
assert tmpConf.docPanePos == [150, 150] assert tmpConf.docPanePos == [150, 150]
tmpConf.guiScale = 1.0 tmpConf.guiScale = 1.0
assert tmpConf.setDocPanePos([300, 300]) tmpConf.setDocPanePos([300, 300])
assert tmpConf.getDocPanePos() == [300, 300] assert tmpConf.getDocPanePos() == [300, 300]
assert tmpConf.docPanePos == [300, 300] assert tmpConf.docPanePos == [300, 300]
assert tmpConf.setDocPanePos([400, 400]) tmpConf.setDocPanePos([400, 400])
# View Pane Splitter # View Pane Splitter
tmpConf.guiScale = 2.0 tmpConf.guiScale = 2.0
assert tmpConf.setViewPanePos([400, 250]) tmpConf.setViewPanePos([400, 250])
assert tmpConf.getViewPanePos() == [400, 250] assert tmpConf.getViewPanePos() == [400, 250]
assert tmpConf.viewPanePos == [200, 125] assert tmpConf.viewPanePos == [200, 125]
tmpConf.guiScale = 1.0 tmpConf.guiScale = 1.0
assert tmpConf.setViewPanePos([400, 250]) tmpConf.setViewPanePos([400, 250])
assert tmpConf.getViewPanePos() == [400, 250] assert tmpConf.getViewPanePos() == [400, 250]
assert tmpConf.viewPanePos == [400, 250] assert tmpConf.viewPanePos == [400, 250]
assert tmpConf.setViewPanePos([500, 150]) tmpConf.setViewPanePos([500, 150])
# Outline Pane Splitter # Outline Pane Splitter
tmpConf.guiScale = 2.0 tmpConf.guiScale = 2.0
assert tmpConf.setOutlinePanePos([400, 250]) tmpConf.setOutlinePanePos([400, 250])
assert tmpConf.getOutlinePanePos() == [400, 250] assert tmpConf.getOutlinePanePos() == [400, 250]
assert tmpConf.outlnPanePos == [200, 125] assert tmpConf.outlnPanePos == [200, 125]
tmpConf.guiScale = 1.0 tmpConf.guiScale = 1.0
assert tmpConf.setOutlinePanePos([400, 250]) tmpConf.setOutlinePanePos([400, 250])
assert tmpConf.getOutlinePanePos() == [400, 250] assert tmpConf.getOutlinePanePos() == [400, 250]
assert tmpConf.outlnPanePos == [400, 250] assert tmpConf.outlnPanePos == [400, 250]
assert tmpConf.setOutlinePanePos([500, 150]) tmpConf.setOutlinePanePos([500, 150])
# Getters Only # Getters Only
# ============ # ============
@@ -429,27 +364,20 @@ def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir):
# Flag Setters # Flag Setters
# ============ # ============
assert tmpConf.setShowRefPanel(False) is False tmpConf.setShowRefPanel(False)
assert tmpConf.showRefPanel is 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.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.viewSynopsis is False
assert tmpConf.setViewSynopsis(True) is True 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)
assert cmpFiles(testFile, compFile, ignoreStart=("timestamp", "lastnotes", "guilang"))
# END Test testBaseConfig_SettersGetters # END Test testBaseConfig_SettersGetters
@@ -480,3 +408,68 @@ def testBaseConfig_Internal(monkeypatch, tmpConf):
assert tmpConf.hasEnchant is False assert tmpConf.hasEnchant is False
# END Test testBaseConfig_Internal # 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
+2 -22
View File
@@ -20,9 +20,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import pytest import pytest
import novelwriter
from PyQt5.QtWidgets import QMessageBox, qApp
from mock import causeException from mock import causeException
@@ -30,18 +27,9 @@ from novelwriter.error import NWErrorMessage, exceptionHandler
@pytest.mark.base @pytest.mark.base
def testBaseError_Dialog(qtbot, monkeypatch, fncDir, tmpDir): def testBaseError_Dialog(qtbot, monkeypatch, nwGUI):
"""Test the error dialog. """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) nwErr = NWErrorMessage(nwGUI)
qtbot.addWidget(nwErr) qtbot.addWidget(nwErr)
nwErr.show() nwErr.show()
@@ -76,19 +64,11 @@ def testBaseError_Dialog(qtbot, monkeypatch, fncDir, tmpDir):
@pytest.mark.base @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 """Test the error handler. This test doesn'thave any asserts, but it
checks that the error handler handles potential exceptions. The test checks that the error handler handles potential exceptions. The test
will fail if excpetions are not handled. 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 # Normal shutdown
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(NWErrorMessage, "exec_", lambda *a: None) mp.setattr(NWErrorMessage, "exec_", lambda *a: None)
+16 -16
View File
@@ -28,13 +28,13 @@ from mock import MockGuiMain
@pytest.mark.base @pytest.mark.base
def testBaseInit_Launch(caplog, monkeypatch, tmpDir): def testBaseInit_Launch(caplog, monkeypatch, tmpPath):
"""Check launching the main GUI. """Check launching the main GUI.
""" """
monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain) monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain)
# TestMode Launch # 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) assert isinstance(nwGUI, MockGuiMain)
# Darwin Launch # Darwin Launch
@@ -43,7 +43,7 @@ def testBaseInit_Launch(caplog, monkeypatch, tmpDir):
novelwriter.CONFIG.osDarwin = True novelwriter.CONFIG.osDarwin = True
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setitem(sys.modules, "Foundation", None) 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 isinstance(nwGUI, MockGuiMain)
assert "Failed" in caplog.text assert "Failed" in caplog.text
@@ -55,7 +55,7 @@ def testBaseInit_Launch(caplog, monkeypatch, tmpDir):
novelwriter.CONFIG.osWindows = True novelwriter.CONFIG.osWindows = True
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setitem(sys.modules, "ctypes", None) 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) assert isinstance(nwGUI, MockGuiMain)
if not sys.platform.startswith("darwin"): if not sys.platform.startswith("darwin"):
# For some reason, the test doesn't work on macOS # 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.setOrganizationDomain", lambda *a: None)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.exec_", lambda *a: 0) monkeypatch.setattr("PyQt5.QtWidgets.QApplication.exec_", lambda *a: 0)
with pytest.raises(SystemExit) as ex: 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 assert ex.value.code == 0
# END Test testBaseInit_Launch # END Test testBaseInit_Launch
@pytest.mark.base @pytest.mark.base
def testBaseInit_Options(monkeypatch, tmpDir): def testBaseInit_Options(monkeypatch, tmpPath):
"""Test command line options for logging level. """Test command line options for logging level.
""" """
monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain) monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain)
monkeypatch.setattr(sys, "argv", [ 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 # Defaults w/None Args
@@ -93,20 +93,20 @@ def testBaseInit_Options(monkeypatch, tmpDir):
# Defaults # Defaults
nwGUI = novelwriter.main( 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 novelwriter.logger.getEffectiveLevel() == logging.WARNING
assert nwGUI.closeMain() == "closeMain" assert nwGUI.closeMain() == "closeMain"
# Log Levels # Log Levels
nwGUI = novelwriter.main( 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 novelwriter.logger.getEffectiveLevel() == logging.INFO
assert nwGUI.closeMain() == "closeMain" assert nwGUI.closeMain() == "closeMain"
nwGUI = novelwriter.main( 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 novelwriter.logger.getEffectiveLevel() == logging.DEBUG
assert nwGUI.closeMain() == "closeMain" assert nwGUI.closeMain() == "closeMain"
@@ -114,14 +114,14 @@ def testBaseInit_Options(monkeypatch, tmpDir):
# Help and Version # Help and Version
with pytest.raises(SystemExit) as ex: with pytest.raises(SystemExit) as ex:
nwGUI = novelwriter.main( nwGUI = novelwriter.main(
["--testmode", "--help", "--config=%s" % tmpDir, "--data=%s" % tmpDir] ["--testmode", "--help", f"--config={tmpPath}", f"--data={tmpPath}"]
) )
assert nwGUI.closeMain() == "closeMain" assert nwGUI.closeMain() == "closeMain"
assert ex.value.code == 0 assert ex.value.code == 0
with pytest.raises(SystemExit) as ex: with pytest.raises(SystemExit) as ex:
nwGUI = novelwriter.main( nwGUI = novelwriter.main(
["--testmode", "--version", "--config=%s" % tmpDir, "--data=%s" % tmpDir] ["--testmode", "--version", f"--config={tmpPath}", f"--data={tmpPath}"]
) )
assert nwGUI.closeMain() == "closeMain" assert nwGUI.closeMain() == "closeMain"
assert ex.value.code == 0 assert ex.value.code == 0
@@ -129,14 +129,14 @@ def testBaseInit_Options(monkeypatch, tmpDir):
# Invalid options # Invalid options
with pytest.raises(SystemExit) as ex: with pytest.raises(SystemExit) as ex:
nwGUI = novelwriter.main( nwGUI = novelwriter.main(
["--testmode", "--invalid", "--config=%s" % tmpDir, "--data=%s" % tmpDir] ["--testmode", "--invalid", f"--config={tmpPath}", f"--data={tmpPath}"]
) )
assert nwGUI.closeMain() == "closeMain" assert nwGUI.closeMain() == "closeMain"
assert ex.value.code == 2 assert ex.value.code == 2
# Project Path # Project Path
nwGUI = novelwriter.main( 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 novelwriter.CONFIG.cmdOpen == "sample/"
assert nwGUI.closeMain() == "closeMain" assert nwGUI.closeMain() == "closeMain"
@@ -145,7 +145,7 @@ def testBaseInit_Options(monkeypatch, tmpDir):
@pytest.mark.base @pytest.mark.base
def testBaseInit_Imports(caplog, monkeypatch, tmpDir): def testBaseInit_Imports(caplog, monkeypatch, tmpPath):
"""Check import error handling. """Check import error handling.
""" """
monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain) monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain)
@@ -161,7 +161,7 @@ def testBaseInit_Imports(caplog, monkeypatch, tmpDir):
with pytest.raises(SystemExit) as ex: with pytest.raises(SystemExit) as ex:
_ = novelwriter.main( _ = 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 assert ex.value.code & 4 == 4 # Python version not satisfied
+39 -39
View File
@@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import uuid import uuid
import pytest import pytest
@@ -35,12 +34,12 @@ from novelwriter.core.coretools import DocMerger, DocSplitter, ProjectBuilder
@pytest.mark.core @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. """Test the DocMerger utility.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncDir) buildTestProject(theProject, fncPath)
# Create Files to Merge # Create Files to Merge
# ===================== # =====================
@@ -77,9 +76,9 @@ def testCoreTools_DocMerger(monkeypatch, mockGUI, fncDir, outDir, refDir, mockRn
# Merge to New # Merge to New
# ============ # ============
saveFile = os.path.join(fncDir, "content", "0000000000014.nwd") saveFile = fncPath / "content" / "0000000000014.nwd"
testFile = os.path.join(outDir, "coreDocTools_DocMerger_0000000000014.nwd") testFile = tstPaths.outDir / "coreDocTools_DocMerger_0000000000014.nwd"
compFile = os.path.join(refDir, "coreDocTools_DocMerger_0000000000014.nwd") compFile = tstPaths.refDir / "coreDocTools_DocMerger_0000000000014.nwd"
assert docMerger.newTargetDoc(hChapter1, "All of Chapter 1") == "0000000000014" 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: with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError) mp.setattr("builtins.open", causeOSError)
assert docMerger.writeTargetDoc() is False assert docMerger.writeTargetDoc() is False
assert not os.path.isfile(saveFile) assert not saveFile.exists()
assert docMerger.getError() != "" assert docMerger.getError() != ""
# Write properly, and compare # Write properly, and compare
@@ -103,9 +102,9 @@ def testCoreTools_DocMerger(monkeypatch, mockGUI, fncDir, outDir, refDir, mockRn
# Merge into Existing # Merge into Existing
# =================== # ===================
saveFile = os.path.join(fncDir, "content", "0000000000010.nwd") saveFile = fncPath / "content" / "0000000000010.nwd"
testFile = os.path.join(outDir, "coreDocTools_DocMerger_0000000000010.nwd") testFile = tstPaths.outDir / "coreDocTools_DocMerger_0000000000010.nwd"
compFile = os.path.join(refDir, "coreDocTools_DocMerger_0000000000010.nwd") compFile = tstPaths.refDir / "coreDocTools_DocMerger_0000000000010.nwd"
docMerger.setTargetDoc(hChapter1) docMerger.setTargetDoc(hChapter1)
@@ -124,12 +123,12 @@ def testCoreTools_DocMerger(monkeypatch, mockGUI, fncDir, outDir, refDir, mockRn
@pytest.mark.core @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. """Test the DocSplitter utility.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncDir) buildTestProject(theProject, fncPath)
# Create File to Split # Create File to Split
# ==================== # ====================
@@ -264,15 +263,15 @@ def testCoreTools_DocSplitter(monkeypatch, mockGUI, fncDir, outDir, refDir, mock
@pytest.mark.core @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 """Create a new project from a project wizard dictionary. With
default setting, creating a Minimal project. default setting, creating a Minimal project.
""" """
monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")) monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed"))
projFile = os.path.join(fncDir, "nwProject.nwx") projFile = fncPath / "nwProject.nwx"
testFile = os.path.join(outDir, "coreTools_NewMinimal_nwProject.nwx") testFile = tstPaths.outDir / "coreTools_NewMinimal_nwProject.nwx"
compFile = os.path.join(refDir, "coreTools_NewMinimal_nwProject.nwx") compFile = tstPaths.refDir / "coreTools_NewMinimal_nwProject.nwx"
projBuild = ProjectBuilder(mockGUI) projBuild = ProjectBuilder(mockGUI)
@@ -283,10 +282,10 @@ def testCoreTools_NewMinimal(monkeypatch, fncDir, outDir, refDir, mockGUI, mockR
assert projBuild.buildProject("stuff") is False assert projBuild.buildProject("stuff") is False
# Try again with a proper path # 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 # Creating the project once more should fail
assert projBuild.buildProject({"projPath": fncDir}) is False assert projBuild.buildProject({"projPath": fncPath}) is False
# Save and close # Save and close
copyfile(projFile, testFile) copyfile(projFile, testFile)
@@ -296,21 +295,21 @@ def testCoreTools_NewMinimal(monkeypatch, fncDir, outDir, refDir, mockGUI, mockR
@pytest.mark.core @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. """Create a new project from a project wizard dictionary.
Custom type with chapters and scenes. Custom type with chapters and scenes.
""" """
monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")) monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed"))
projFile = os.path.join(fncDir, "nwProject.nwx") projFile = fncPath / "nwProject.nwx"
testFile = os.path.join(outDir, "coreTools_NewCustomA_nwProject.nwx") testFile = tstPaths.outDir / "coreTools_NewCustomA_nwProject.nwx"
compFile = os.path.join(refDir, "coreTools_NewCustomA_nwProject.nwx") compFile = tstPaths.refDir / "coreTools_NewCustomA_nwProject.nwx"
projData = { projData = {
"projName": "Test Custom", "projName": "Test Custom",
"projTitle": "Test Novel", "projTitle": "Test Novel",
"projAuthors": "Jane Doe\nJohn Doh\n", "projAuthors": "Jane Doe\nJohn Doh\n",
"projPath": fncDir, "projPath": fncPath,
"popSample": False, "popSample": False,
"popMinimal": False, "popMinimal": False,
"popCustom": True, "popCustom": True,
@@ -334,21 +333,21 @@ def testCoreTools_NewCustomA(monkeypatch, fncDir, outDir, refDir, mockGUI, mockR
@pytest.mark.core @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. """Create a new project from a project wizard dictionary.
Custom type without chapters, but with scenes. Custom type without chapters, but with scenes.
""" """
monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed")) monkeypatch.setattr("uuid.uuid4", lambda *a: uuid.UUID("d0f3fe10-c6e6-4310-8bfd-181eb4224eed"))
projFile = os.path.join(fncDir, "nwProject.nwx") projFile = fncPath / "nwProject.nwx"
testFile = os.path.join(outDir, "coreTools_NewCustomB_nwProject.nwx") testFile = tstPaths.outDir / "coreTools_NewCustomB_nwProject.nwx"
compFile = os.path.join(refDir, "coreTools_NewCustomB_nwProject.nwx") compFile = tstPaths.refDir / "coreTools_NewCustomB_nwProject.nwx"
projData = { projData = {
"projName": "Test Custom", "projName": "Test Custom",
"projTitle": "Test Novel", "projTitle": "Test Novel",
"projAuthors": "Jane Doe\nJohn Doh\n", "projAuthors": "Jane Doe\nJohn Doh\n",
"projPath": fncDir, "projPath": fncPath,
"popSample": False, "popSample": False,
"popMinimal": False, "popMinimal": False,
"popCustom": True, "popCustom": True,
@@ -372,7 +371,7 @@ def testCoreTools_NewCustomB(monkeypatch, fncDir, outDir, refDir, mockGUI, mockR
@pytest.mark.core @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 """Check that we can create a new project can be created from the
provided sample project via a zip file. provided sample project via a zip file.
""" """
@@ -380,7 +379,7 @@ def testCoreTools_NewSample(fncDir, tmpConf, mockGUI, tmpDir):
"projName": "Test Sample", "projName": "Test Sample",
"projTitle": "Test Novel", "projTitle": "Test Novel",
"projAuthors": "Jane Doe\nJohn Doh\n", "projAuthors": "Jane Doe\nJohn Doh\n",
"projPath": fncDir, "projPath": fncPath,
"popSample": True, "popSample": True,
"popMinimal": False, "popMinimal": False,
"popCustom": False, "popCustom": False,
@@ -392,9 +391,11 @@ def testCoreTools_NewSample(fncDir, tmpConf, mockGUI, tmpDir):
assert projBuild.buildProject({"popSample": True}) is False assert projBuild.buildProject({"popSample": True}) is False
# Force the lookup path for assets to our temp folder # Force the lookup path for assets to our temp folder
srcSample = os.path.abspath(os.path.join(tmpConf.appRoot, "sample")) srcSample = tmpConf._appRoot / "sample"
dstSample = os.path.join(tmpDir, "sample.zip") dstSample = tmpPath / "sample.zip"
tmpConf.assetPath = tmpDir monkeypatch.setattr(
"novelwriter.config.Config.assetPath", lambda *a: tmpPath / "sample.zip"
)
# Cannot extract when the zip does not exist # Cannot extract when the zip does not exist
assert projBuild.buildProject(projData) is False assert projBuild.buildProject(projData) is False
@@ -404,16 +405,15 @@ def testCoreTools_NewSample(fncDir, tmpConf, mockGUI, tmpDir):
outFile.write("foo") outFile.write("foo")
assert projBuild.buildProject(projData) is False assert projBuild.buildProject(projData) is False
os.unlink(dstSample) dstSample.unlink()
# Create a real zip file, and unpack it # Create a real zip file, and unpack it
with ZipFile(dstSample, "w") as zipObj: with ZipFile(dstSample, "w") as zipObj:
zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx") zipObj.write(srcSample / "nwProject.nwx", "nwProject.nwx")
for docFile in os.listdir(os.path.join(srcSample, "content")): for docFile in (srcSample / "content").iterdir():
srcDoc = os.path.join(srcSample, "content", docFile) zipObj.write(docFile, f"content/{docFile.name}")
zipObj.write(srcDoc, "content/"+docFile)
assert projBuild.buildProject(projData) is True assert projBuild.buildProject(projData) is True
os.unlink(dstSample) dstSample.unlink()
# END Test testCoreTools_NewSample # END Test testCoreTools_NewSample
+11 -12
View File
@@ -23,7 +23,6 @@ import json
import pytest import pytest
from shutil import copyfile from shutil import copyfile
from pathlib import Path
from mock import causeException from mock import causeException
from tools import C, buildTestProject, cmpFiles, writeFile from tools import C, buildTestProject, cmpFiles, writeFile
@@ -35,16 +34,16 @@ from novelwriter.core.project import NWProject
@pytest.mark.core @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 """Test core functionality of scaning, saving, loading and checking
the index cache file. the index cache file.
""" """
projFile = Path(nwLipsum) / "meta" / nwFiles.INDEX_FILE projFile = prjLipsum / "meta" / nwFiles.INDEX_FILE
testFile = tstPaths.outDir / "coreIndex_LoadSave_tagsIndex.json" testFile = tstPaths.outDir / "coreIndex_LoadSave_tagsIndex.json"
compFile = tstPaths.refDir / "coreIndex_LoadSave_tagsIndex.json" compFile = tstPaths.refDir / "coreIndex_LoadSave_tagsIndex.json"
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
assert theProject.openProject(nwLipsum) assert theProject.openProject(prjLipsum)
theIndex = NWIndex(theProject) theIndex = NWIndex(theProject)
assert repr(theIndex) == "<NWIndex project='Lorem Ipsum'>" assert repr(theIndex) == "<NWIndex project='Lorem Ipsum'>"
@@ -196,12 +195,12 @@ def testCoreIndex_ScanThis(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreIndex_CheckThese(mockGUI, fncDir, mockRnd): def testCoreIndex_CheckThese(mockGUI, fncPath, mockRnd):
"""Test the tag checker function checkThese. """Test the tag checker function checkThese.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncDir) buildTestProject(theProject, fncPath)
theIndex = theProject.index theIndex = theProject.index
theIndex.clearIndex() theIndex.clearIndex()
@@ -274,12 +273,12 @@ def testCoreIndex_CheckThese(mockGUI, fncDir, mockRnd):
@pytest.mark.core @pytest.mark.core
def testCoreIndex_ScanText(mockGUI, fncDir, mockRnd): def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd):
"""Check the index text scanner. """Check the index text scanner.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncDir) buildTestProject(theProject, fncPath)
theIndex = theProject.index theIndex = theProject.index
# Some items for fail to scan tests # Some items for fail to scan tests
@@ -486,12 +485,12 @@ def testCoreIndex_ScanText(mockGUI, fncDir, mockRnd):
@pytest.mark.core @pytest.mark.core
def testCoreIndex_ExtractData(mockGUI, fncDir, mockRnd): def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
"""Check the index data extraction functions. """Check the index data extraction functions.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncDir) buildTestProject(theProject, fncPath)
theIndex = theProject.index theIndex = theProject.index
theIndex.reIndexHandle(C.hNovelRoot) theIndex.reIndexHandle(C.hNovelRoot)
@@ -940,12 +939,12 @@ def testCoreIndex_TagsIndex():
@pytest.mark.core @pytest.mark.core
def testCoreIndex_ItemIndex(mockGUI, fncDir, mockRnd): def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
"""Check the ItemIndex class. """Check the ItemIndex class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncDir) buildTestProject(theProject, fncPath)
theProject.index.clearIndex() theProject.index.clearIndex()
nHandle = C.hTitlePage nHandle = C.hTitlePage
+4 -4
View File
@@ -31,12 +31,12 @@ from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
@pytest.mark.core @pytest.mark.core
def testCoreItem_Setters(mockGUI, mockRnd, fncDir): def testCoreItem_Setters(mockGUI, mockRnd, fncPath):
"""Test all the simple setters for the NWItem class. """Test all the simple setters for the NWItem class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncDir) buildTestProject(theProject, fncPath)
theItem = NWItem(theProject) theItem = NWItem(theProject)
statusKeys = ["s000000", "s000001", "s000002", "s000003"] statusKeys = ["s000000", "s000001", "s000002", "s000003"]
@@ -192,12 +192,12 @@ def testCoreItem_Setters(mockGUI, mockRnd, fncDir):
@pytest.mark.core @pytest.mark.core
def testCoreItem_Methods(mockGUI, mockRnd, fncDir): def testCoreItem_Methods(mockGUI, mockRnd, fncPath):
"""Test the simple methods of the NWItem class. """Test the simple methods of the NWItem class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncDir) buildTestProject(theProject, fncPath)
theItem = NWItem(theProject) theItem = NWItem(theProject)
# Describe Me # Describe Me
+14 -12
View File
@@ -180,6 +180,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
# Fail on lock file # Fail on lock file
assert theProject._storage.writeLockFile() assert theProject._storage.writeLockFile()
assert theProject.openProject(fncPath) is False assert theProject.openProject(fncPath) is False
assert isinstance(theProject.getLockStatus(), list)
# Fail to read lockfile (which still opens the project) # Fail to read lockfile (which still opens the project)
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
@@ -193,6 +194,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
assert theProject._storage.writeLockFile() assert theProject._storage.writeLockFile()
assert theProject.openProject(fncPath, overrideLock=True) is True assert theProject.openProject(fncPath, overrideLock=True) is True
assert theProject.closeProject() assert theProject.closeProject()
assert theProject.getLockStatus() is None
# Fail getting xml reader # Fail getting xml reader
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
@@ -625,7 +627,7 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
@pytest.mark.core @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 """Check that files in the content folder that are not tracked in
the project XML file are handled correctly by the orphaned files the project XML file are handled correctly by the orphaned files
function. It should also restore as much meta data as possible from function. It should also restore as much meta data as possible from
@@ -633,7 +635,7 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum):
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
assert theProject.openProject(nwLipsum) is True assert theProject.openProject(prjLipsum) is True
assert theProject.tree["636b6aa9b697b"] is None assert theProject.tree["636b6aa9b697b"] is None
# Add a file with non-existent parent # Add a file with non-existent parent
@@ -646,7 +648,7 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum):
assert theProject.closeProject() is True assert theProject.closeProject() is True
# First Item with Meta Data # First Item with Meta Data
orphPath = Path(nwLipsum) / "content" / "636b6aa9b697b.nwd" orphPath = prjLipsum / "content" / "636b6aa9b697b.nwd"
writeFile(orphPath, ( writeFile(orphPath, (
"%%~name:[Recovered] Mars\n" "%%~name:[Recovered] Mars\n"
"%%~path:5eaea4e8cdee8/636b6aa9b697b\n" "%%~path:5eaea4e8cdee8/636b6aa9b697b\n"
@@ -656,22 +658,22 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum):
)) ))
# Second Item without Meta Data # Second Item without Meta Data
orphPath = Path(nwLipsum) / "content" / "736b6aa9b697b.nwd" orphPath = prjLipsum / "content" / "736b6aa9b697b.nwd"
writeFile(orphPath, "\n") writeFile(orphPath, "\n")
# Invalid File Name # Invalid File Name
tstPath = Path(nwLipsum) / "content" / "636b6aa9b697b.txt" tstPath = prjLipsum / "content" / "636b6aa9b697b.txt"
writeFile(tstPath, "\n") writeFile(tstPath, "\n")
# Invalid File Name # Invalid File Name
tstPath = Path(nwLipsum) / "content" / "636b6aa9b697bb.nwd" tstPath = prjLipsum / "content" / "636b6aa9b697bb.nwd"
writeFile(tstPath, "\n") writeFile(tstPath, "\n")
# Invalid File Name # Invalid File Name
tstPath = Path(nwLipsum) / "content" / "abcdefghijklm.nwd" tstPath = prjLipsum / "content" / "abcdefghijklm.nwd"
writeFile(tstPath, "\n") writeFile(tstPath, "\n")
assert theProject.openProject(nwLipsum) assert theProject.openProject(prjLipsum)
assert theProject.storage.storagePath is not None assert theProject.storage.storagePath is not None
assert theProject.storage.runtimePath is not None assert theProject.storage.runtimePath is not None
assert theProject.tree["636b6aa9b697bb"] is None assert theProject.tree["636b6aa9b697bb"] is None
@@ -697,7 +699,7 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum):
assert oItem.itemType == nwItemType.FILE assert oItem.itemType == nwItemType.FILE
assert oItem.itemLayout == nwItemLayout.NOTE assert oItem.itemLayout == nwItemLayout.NOTE
assert theProject.saveProject(nwLipsum) assert theProject.saveProject(prjLipsum)
assert theProject.closeProject() assert theProject.closeProject()
# Finally, check that the orphaned files function returns # Finally, check that the orphaned files function returns
@@ -730,17 +732,17 @@ def testCoreProject_Backup(monkeypatch, mockGUI, fncPath, tmpPath):
mockGUI.hasProject = True mockGUI.hasProject = True
# Invalid path # Invalid path
theProject.mainConf.backupPath = None theProject.mainConf._backupPath = None
assert theProject.backupProject(doNotify=False) is False assert theProject.backupProject(doNotify=False) is False
# Missing project name # Missing project name
theProject.mainConf.backupPath = str(tmpPath) theProject.mainConf._backupPath = tmpPath
theProject.data.setName("") theProject.data.setName("")
assert theProject.backupProject(doNotify=False) is False assert theProject.backupProject(doNotify=False) is False
# Valid Settings # Valid Settings
# ============== # ==============
theProject.mainConf.backupPath = str(tmpPath) theProject.mainConf._backupPath = tmpPath
theProject.data.setName("Test Minimal") theProject.data.setName("Test Minimal")
# Can't make folder # Can't make folder
+2 -3
View File
@@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import pytest import pytest
from tools import readFile from tools import readFile
@@ -441,7 +440,7 @@ def testCoreToHtml_SpecialCases(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreToHtml_Complex(mockGUI, fncDir): def testCoreToHtml_Complex(mockGUI, fncPath):
"""Test the save method of the ToHtml class. """Test the save method of the ToHtml class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
@@ -529,7 +528,7 @@ def testCoreToHtml_Complex(mockGUI, fncDir):
bodyText="".join(resText).rstrip() bodyText="".join(resText).rstrip()
) )
saveFile = os.path.join(fncDir, "outFile.htm") saveFile = fncPath / "outFile.htm"
theHtml.saveHTML5(saveFile) theHtml.saveHTML5(saveFile)
assert readFile(saveFile) == htmlDoc assert readFile(saveFile) == htmlDoc
+3 -4
View File
@@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import pytest import pytest
from tools import C, buildTestProject, readFile from tools import C, buildTestProject, readFile
@@ -132,12 +131,12 @@ def testCoreToken_Setters(mockGUI):
@pytest.mark.core @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. """Test handling files and text in the Tokenizer class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncDir) buildTestProject(theProject, fncPath)
theProject.data.setLanguage("en") theProject.data.setLanguage("en")
theProject._loadProjectLocalisation() theProject._loadProjectLocalisation()
@@ -210,7 +209,7 @@ def testCoreToken_TextOps(monkeypatch, mockGUI, mockRnd, fncDir):
assert theToken.theResult == "This is text with escapes: ** ~~ __" assert theToken.theResult == "This is text with escapes: ** ~~ __"
# Save File # Save File
savePath = os.path.join(fncDir, "dump.nwd") savePath = fncPath / "dump.nwd"
theToken.saveRawMarkdown(savePath) theToken.saveRawMarkdown(savePath)
assert readFile(savePath) == ( assert readFile(savePath) == (
"# Notes: Plot\n\n" "# Notes: Plot\n\n"
+2 -3
View File
@@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import pytest import pytest
from tools import readFile from tools import readFile
@@ -208,7 +207,7 @@ def testCoreToMarkdown_ConvertDirect(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreToMarkdown_Complex(mockGUI, fncDir): def testCoreToMarkdown_Complex(mockGUI, fncPath):
"""Test the save method of the ToMarkdown class. """Test the save method of the ToMarkdown class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
@@ -253,7 +252,7 @@ def testCoreToMarkdown_Complex(mockGUI, fncDir):
# Check File # Check File
# ========== # ==========
saveFile = os.path.join(fncDir, "outFile.md") saveFile = fncPath / "outFile.md"
theMD.saveMarkdown(saveFile) theMD.saveMarkdown(saveFile)
assert readFile(saveFile) == "".join(resText) assert readFile(saveFile) == "".join(resText)
+25 -26
View File
@@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import pytest import pytest
import zipfile import zipfile
@@ -612,7 +611,7 @@ def testCoreToOdt_ConvertDirect(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreToOdt_SaveFlat(mockGUI, fncDir, outDir, refDir): def testCoreToOdt_SaveFlat(mockGUI, fncPath, tstPaths):
"""Test the document save functions. """Test the document save functions.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
@@ -634,12 +633,12 @@ def testCoreToOdt_SaveFlat(mockGUI, fncDir, outDir, refDir):
theDoc.doConvert() theDoc.doConvert()
theDoc.closeDocument() theDoc.closeDocument()
flatFile = os.path.join(fncDir, "document.fodt") flatFile = fncPath / "document.fodt"
testFile = os.path.join(outDir, "coreToOdt_SaveFlat_document.fodt") testFile = tstPaths.outDir / "coreToOdt_SaveFlat_document.fodt"
compFile = os.path.join(refDir, "coreToOdt_SaveFlat_document.fodt") compFile = tstPaths.refDir / "coreToOdt_SaveFlat_document.fodt"
theDoc.saveFlatXML(flatFile) theDoc.saveFlatXML(flatFile)
assert os.path.isfile(flatFile) assert flatFile.exists()
copyfile(flatFile, testFile) copyfile(flatFile, testFile)
assert cmpFiles(testFile, compFile, [4, 5]) assert cmpFiles(testFile, compFile, [4, 5])
@@ -648,7 +647,7 @@ def testCoreToOdt_SaveFlat(mockGUI, fncDir, outDir, refDir):
@pytest.mark.core @pytest.mark.core
def testCoreToOdt_SaveFull(mockGUI, fncDir, outDir, refDir): def testCoreToOdt_SaveFull(mockGUI, fncPath, tstPaths):
"""Test the document save functions. """Test the document save functions.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
@@ -667,25 +666,25 @@ def testCoreToOdt_SaveFull(mockGUI, fncDir, outDir, refDir):
theDoc.doConvert() theDoc.doConvert()
theDoc.closeDocument() theDoc.closeDocument()
fullFile = os.path.join(fncDir, "document.odt") fullFile = fncPath / "document.odt"
theDoc.saveOpenDocText(fullFile) theDoc.saveOpenDocText(fullFile)
assert os.path.isfile(fullFile) assert fullFile.exists()
assert zipfile.is_zipfile(fullFile) assert zipfile.is_zipfile(fullFile)
maniFile = os.path.join(outDir, "coreToOdt_SaveFull_manifest.xml") maniFile = tstPaths.outDir / "coreToOdt_SaveFull_manifest.xml"
settFile = os.path.join(outDir, "coreToOdt_SaveFull_settings.xml") settFile = tstPaths.outDir / "coreToOdt_SaveFull_settings.xml"
contFile = os.path.join(outDir, "coreToOdt_SaveFull_content.xml") contFile = tstPaths.outDir / "coreToOdt_SaveFull_content.xml"
metaFile = os.path.join(outDir, "coreToOdt_SaveFull_meta.xml") metaFile = tstPaths.outDir / "coreToOdt_SaveFull_meta.xml"
stylFile = os.path.join(outDir, "coreToOdt_SaveFull_styles.xml") stylFile = tstPaths.outDir / "coreToOdt_SaveFull_styles.xml"
maniComp = os.path.join(refDir, "coreToOdt_SaveFull_manifest.xml") maniComp = tstPaths.refDir / "coreToOdt_SaveFull_manifest.xml"
settComp = os.path.join(refDir, "coreToOdt_SaveFull_settings.xml") settComp = tstPaths.refDir / "coreToOdt_SaveFull_settings.xml"
contComp = os.path.join(refDir, "coreToOdt_SaveFull_content.xml") contComp = tstPaths.refDir / "coreToOdt_SaveFull_content.xml"
metaComp = os.path.join(refDir, "coreToOdt_SaveFull_meta.xml") metaComp = tstPaths.refDir / "coreToOdt_SaveFull_meta.xml"
stylComp = os.path.join(refDir, "coreToOdt_SaveFull_styles.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: with zipfile.ZipFile(fullFile, mode="r") as theZip:
theZip.extract("META-INF/manifest.xml", extaxtTo) 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("meta.xml", extaxtTo)
theZip.extract("styles.xml", extaxtTo) theZip.extract("styles.xml", extaxtTo)
maniOut = os.path.join(outDir, "coreToOdt_SaveFull", "META-INF", "manifest.xml") maniOut = tstPaths.outDir / "coreToOdt_SaveFull" / "META-INF" / "manifest.xml"
settOut = os.path.join(outDir, "coreToOdt_SaveFull", "settings.xml") settOut = tstPaths.outDir / "coreToOdt_SaveFull" / "settings.xml"
contOut = os.path.join(outDir, "coreToOdt_SaveFull", "content.xml") contOut = tstPaths.outDir / "coreToOdt_SaveFull" / "content.xml"
metaOut = os.path.join(outDir, "coreToOdt_SaveFull", "meta.xml") metaOut = tstPaths.outDir / "coreToOdt_SaveFull" / "meta.xml"
stylOut = os.path.join(outDir, "coreToOdt_SaveFull", "styles.xml") stylOut = tstPaths.outDir / "coreToOdt_SaveFull" / "styles.xml"
def prettifyXml(inFile, outFile): def prettifyXml(inFile, outFile):
with open(outFile, mode="wb") as fileStream: with open(outFile, mode="wb") as fileStream:
fileStream.write( fileStream.write(
etree.tostring( etree.tostring(
etree.parse(inFile), etree.parse(str(inFile)),
pretty_print=True, pretty_print=True,
encoding="utf-8", encoding="utf-8",
xml_declaration=True xml_declaration=True
+9 -8
View File
@@ -21,6 +21,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import pytest import pytest
from pathlib import Path
from tools import getGuiItem from tools import getGuiItem
from PyQt5.QtWidgets import QAction, QMessageBox from PyQt5.QtWidgets import QAction, QMessageBox
@@ -29,7 +31,7 @@ from novelwriter.dialogs.about import GuiAbout
@pytest.mark.gui @pytest.mark.gui
def testDlgAbout_NWDialog(qtbot, nwGUI): def testDlgAbout_NWDialog(qtbot, monkeypatch, nwGUI):
"""Test the novelWriter about dialogs. """Test the novelWriter about dialogs.
""" """
# NW About # NW About
@@ -45,13 +47,12 @@ def testDlgAbout_NWDialog(qtbot, nwGUI):
assert msgAbout.pageNotes.document().characterCount() > 100 assert msgAbout.pageNotes.document().characterCount() > 100
assert msgAbout.pageLicense.document().characterCount() > 100 assert msgAbout.pageLicense.document().characterCount() > 100
msgAbout.mainConf.assetPath = "whatever" with monkeypatch.context() as mp:
mp.setattr("novelwriter.config.Config.assetPath", lambda *a: Path("whatever"))
msgAbout._fillNotesPage() msgAbout._fillNotesPage()
assert msgAbout.pageNotes.toPlainText() == "Error loading release notes text ..." assert msgAbout.pageNotes.toPlainText() == "Error loading release notes text ..."
msgAbout._fillLicensePage()
msgAbout._fillLicensePage() assert msgAbout.pageLicense.toPlainText() == "Error loading licence text ..."
assert msgAbout.pageLicense.toPlainText() == "Error loading licence text ..."
msgAbout.showReleaseNotes() msgAbout.showReleaseNotes()
assert msgAbout.tabBox.currentWidget() == msgAbout.pageNotes assert msgAbout.tabBox.currentWidget() == msgAbout.pageNotes
+2 -2
View File
@@ -29,11 +29,11 @@ from novelwriter.dialogs.docmerge import GuiDocMerge
@pytest.mark.gui @pytest.mark.gui
def testDlgMerge_Main(qtbot, nwGUI, fncProj, mockRnd): def testDlgMerge_Main(qtbot, nwGUI, projPath, mockRnd):
"""Test the merge documents tool. """Test the merge documents tool.
""" """
# Create a new project # Create a new project
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
# Check that the dialog kan handle invalid items # Check that the dialog kan handle invalid items
nwMerge = GuiDocMerge(nwGUI, C.hInvalid, [C.hInvalid]) nwMerge = GuiDocMerge(nwGUI, C.hInvalid, [C.hInvalid])
+2 -2
View File
@@ -28,13 +28,13 @@ from novelwriter.dialogs.editlabel import GuiEditLabel
@pytest.mark.gui @pytest.mark.gui
def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test the split document tool. """Test the split document tool.
""" """
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
# Create a new project # Create a new project
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
theProject = nwGUI.theProject theProject = nwGUI.theProject
projTree = nwGUI.projView.projTree projTree = nwGUI.projView.projTree
+9 -33
View File
@@ -19,19 +19,17 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import pytest import pytest
import novelwriter
from shutil import copyfile from shutil import copyfile
from tools import cmpFiles, getGuiItem from tools import cmpFiles, getGuiItem
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import ( 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.quotes import GuiQuoteSelect
from novelwriter.dialogs.preferences import GuiPreferences from novelwriter.dialogs.preferences import GuiPreferences
@@ -39,31 +37,11 @@ KEY_DELAY = 1
@pytest.mark.gui @pytest.mark.gui
def testDlgPreferences_Main(qtbot, monkeypatch, fncDir, outDir, refDir): def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
"""Test the load project wizard. """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 theConf = nwGUI.mainConf
assert theConf.confPath == fncDir assert theConf._confPath == fncPath
monkeypatch.setattr(GuiPreferences, "exec_", lambda *a: None) monkeypatch.setattr(GuiPreferences, "exec_", lambda *a: None)
monkeypatch.setattr(GuiPreferences, "result", lambda *a: QDialog.Accepted) monkeypatch.setattr(GuiPreferences, "result", lambda *a: QDialog.Accepted)
@@ -80,7 +58,7 @@ def testDlgPreferences_Main(qtbot, monkeypatch, fncDir, outDir, refDir):
nwPrefs = getGuiItem("GuiPreferences") nwPrefs = getGuiItem("GuiPreferences")
assert isinstance(nwPrefs, GuiPreferences) assert isinstance(nwPrefs, GuiPreferences)
nwPrefs.show() nwPrefs.show()
assert nwPrefs.mainConf.confPath == fncDir assert nwPrefs.mainConf._confPath == fncPath
assert nwPrefs.updateTheme is False assert nwPrefs.updateTheme is False
assert nwPrefs.updateSyntax is False assert nwPrefs.updateSyntax is False
@@ -238,22 +216,20 @@ def testDlgPreferences_Main(qtbot, monkeypatch, fncDir, outDir, refDir):
nwPrefs._doClose() nwPrefs._doClose()
assert theConf.confChanged assert theConf.confChanged
theConf.lastPath = ""
assert nwGUI.mainConf.saveConfig() assert nwGUI.mainConf.saveConfig()
projFile = os.path.join(fncDir, "novelwriter.conf") projFile = fncPath / "novelwriter.conf"
testFile = os.path.join(outDir, "guiPreferences_novelwriter.conf") testFile = tstPaths.outDir / "guiPreferences_novelwriter.conf"
compFile = os.path.join(refDir, "guiPreferences_novelwriter.conf") compFile = tstPaths.refDir / "guiPreferences_novelwriter.conf"
copyfile(projFile, testFile) copyfile(projFile, testFile)
ignTuple = ( ignTuple = (
"timestamp", "guifont", "lastnotes", "guilang", "geometry", "timestamp", "guifont", "lastnotes", "guilang", "geometry",
"preferences", "projcols", "mainpane", "docpane", "viewpane", "preferences", "projcols", "mainpane", "docpane", "viewpane",
"outlinepane", "textfont", "textsize" "outlinepane", "textfont", "textsize", "lastpath", "backuppath"
) )
assert cmpFiles(testFile, compFile, ignoreStart=ignTuple) assert cmpFiles(testFile, compFile, ignoreStart=ignTuple)
# Clean up # Clean up
novelwriter.CONFIG = origConf
nwGUI.closeMain() nwGUI.closeMain()
# qtbot.stop() # qtbot.stop()
@@ -38,7 +38,6 @@ def testDlgProjDetails_Dialog(qtbot, nwGUI, nwLipsum):
qtbot.wait(100) qtbot.wait(100)
# Open the Writing Stats dialog # Open the Writing Stats dialog
nwGUI.mainConf.lastPath = ""
nwGUI.mainMenu.aProjectDetails.activate(QAction.Trigger) nwGUI.mainMenu.aProjectDetails.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiProjectDetails") is not None, timeout=1000) qtbot.waitUntil(lambda: getGuiItem("GuiProjectDetails") is not None, timeout=1000)
+4 -5
View File
@@ -20,7 +20,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import pytest import pytest
import os
from tools import buildTestProject, getGuiItem from tools import buildTestProject, getGuiItem
@@ -33,10 +32,10 @@ from novelwriter.dialogs.projload import GuiProjectLoad
@pytest.mark.gui @pytest.mark.gui
def testDlgLoadProject_Main(qtbot, monkeypatch, nwGUI, fncProj): def testDlgLoadProject_Main(qtbot, monkeypatch, nwGUI, projPath):
"""Test the load project wizard. """Test the load project wizard.
""" """
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
assert nwGUI.closeProject() assert nwGUI.closeProject()
monkeypatch.setattr(GuiProjectLoad, "exec_", lambda *a: None) monkeypatch.setattr(GuiProjectLoad, "exec_", lambda *a: None)
@@ -87,10 +86,10 @@ def testDlgLoadProject_Main(qtbot, monkeypatch, nwGUI, fncProj):
nwLoad._doDeleteRecent() nwLoad._doDeleteRecent()
assert nwLoad.listBox.topLevelItemCount() == recentCount - 1 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)) monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (getFile, None))
qtbot.mouseClick(nwLoad.browseButton, Qt.LeftButton) qtbot.mouseClick(nwLoad.browseButton, Qt.LeftButton)
assert nwLoad.openPath == fncProj assert nwLoad.openPath == projPath / "nwProject.nwx"
assert nwLoad.openState == nwLoad.OPEN_STATE assert nwLoad.openState == nwLoad.OPEN_STATE
nwLoad.close() nwLoad.close()
+9 -9
View File
@@ -82,16 +82,16 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI):
@pytest.mark.gui @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. """Test the main tab of the project settings dialog.
""" """
# Mock components # Mock components
monkeypatch.setattr(nwGUI.docEditor.spEnchant, "listDictionaries", lambda: [("en", "none")]) monkeypatch.setattr(nwGUI.docEditor.spEnchant, "listDictionaries", lambda: [("en", "none")])
# Create new project # Create new project
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
mockRnd.reset() mockRnd.reset()
nwGUI.mainConf.backupPath = fncDir nwGUI.mainConf.backupPath = fncPath
# Set some values # Set some values
theProject = nwGUI.theProject theProject = nwGUI.theProject
@@ -148,7 +148,7 @@ def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd
@pytest.mark.gui @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 """Test the status and importance tabs of the project settings
dialog. dialog.
""" """
@@ -159,8 +159,8 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncDir, fncProj,
# Create new project # Create new project
mockRnd.reset() mockRnd.reset()
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
nwGUI.mainConf.backupPath = fncDir nwGUI.mainConf.backupPath = fncPath
# Set some values # Set some values
theProject = nwGUI.theProject theProject = nwGUI.theProject
@@ -350,7 +350,7 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncDir, fncProj,
@pytest.mark.gui @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. """Test the auto-replace tab of the project settings dialog.
""" """
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) 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 # Create new project
mockRnd.reset() mockRnd.reset()
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
nwGUI.mainConf.backupPath = fncDir nwGUI.mainConf.backupPath = fncPath
# Set some values # Set some values
theProject = nwGUI.theProject theProject = nwGUI.theProject
+4 -5
View File
@@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import pytest import pytest
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
@@ -33,18 +32,18 @@ from novelwriter.dialogs.wordlist import GuiWordList
@pytest.mark.gui @pytest.mark.gui
def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, fncProj): def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, projPath):
"""test the word list editor. """test the word list editor.
""" """
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
monkeypatch.setattr(GuiWordList, "exec_", lambda *a: None) monkeypatch.setattr(GuiWordList, "exec_", lambda *a: None)
monkeypatch.setattr(GuiWordList, "result", lambda *a: QDialog.Accepted) monkeypatch.setattr(GuiWordList, "result", lambda *a: QDialog.Accepted)
monkeypatch.setattr(GuiWordList, "accept", lambda *a: None) monkeypatch.setattr(GuiWordList, "accept", lambda *a: None)
# Open project # Open project
nwGUI.openProject(fncProj) nwGUI.openProject(projPath)
dictFile = os.path.join(fncProj, "meta", nwFiles.PROJ_DICT) dictFile = projPath / "meta" / nwFiles.PROJ_DICT
# Load the dialog # Load the dialog
nwGUI.mainMenu.aEditWordList.activate(QAction.Trigger) nwGUI.mainMenu.aEditWordList.activate(QAction.Trigger)
+20 -20
View File
@@ -37,11 +37,11 @@ KEY_DELAY = 1
@pytest.mark.gui @pytest.mark.gui
def testGuiEditor_Init(qtbot, nwGUI, fncProj, ipsumText, mockRnd): def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd):
"""Test initialising the editor. """Test initialising the editor.
""" """
# Open project # Open project
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
assert nwGUI.openDocument(C.hSceneDoc) assert nwGUI.openDocument(C.hSceneDoc)
nwGUI.docEditor.setText("### Lorem Ipsum\n\n%s" % ipsumText[0]) 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 @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. """Test loading text into the editor.
""" """
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
assert nwGUI.openDocument(C.hSceneDoc) is True assert nwGUI.openDocument(C.hSceneDoc) is True
longText = "### Lorem Ipsum\n\n%s" % "\n\n".join(ipsumText*20) 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 @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. """Test saving text from the editor.
""" """
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
assert nwGUI.openDocument(C.hSceneDoc) is True assert nwGUI.openDocument(C.hSceneDoc) is True
# Save Text # Save Text
@@ -179,10 +179,10 @@ def testGuiEditor_SaveText(qtbot, monkeypatch, caplog, nwGUI, fncProj, ipsumText
@pytest.mark.gui @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. """Test extracting various meta data and other values.
""" """
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
assert nwGUI.openDocument(C.hSceneDoc) is True assert nwGUI.openDocument(C.hSceneDoc) is True
# Get Text # Get Text
@@ -226,13 +226,13 @@ def testGuiEditor_MetaData(qtbot, nwGUI, fncProj, mockRnd):
@pytest.mark.gui @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 """Test the document actions. This is not an extensive test of the
action features, just that the actions are actually called. The action features, just that the actions are actually called. The
various action features are tested when their respective functions various action features are tested when their respective functions
are tested. are tested.
""" """
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
assert nwGUI.openDocument(C.hSceneDoc) is True assert nwGUI.openDocument(C.hSceneDoc) is True
theText = "### A Scene\n\n%s" % "\n\n".join(ipsumText) 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 @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. """Test the document insert functions.
""" """
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
assert nwGUI.openDocument(C.hSceneDoc) is True assert nwGUI.openDocument(C.hSceneDoc) is True
theText = "### A Scene\n\n%s" % "\n\n".join(ipsumText) 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 @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. """Test the text manipulation functions.
""" """
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
assert nwGUI.openDocument(C.hSceneDoc) is True assert nwGUI.openDocument(C.hSceneDoc) is True
theText = "### A Scene\n\n%s" % "\n\n".join(ipsumText) 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 @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. """Test the block formatting function.
""" """
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
assert nwGUI.openDocument(C.hSceneDoc) is True assert nwGUI.openDocument(C.hSceneDoc) is True
theText = "### A Scene\n\n%s" % "\n\n".join(ipsumText) 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 @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. """Test the document editor tags functionality.
""" """
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
assert nwGUI.openDocument(C.hSceneDoc) is True assert nwGUI.openDocument(C.hSceneDoc) is True
# Create Scene # Create Scene
@@ -1121,7 +1121,7 @@ def testGuiEditor_Tags(qtbot, nwGUI, fncProj, ipsumText, mockRnd):
@pytest.mark.gui @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. """Test saving text from the editor.
""" """
class MockThreadPool: class MockThreadPool:
@@ -1139,7 +1139,7 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, fncProj, ipsumText, mo
nwGUI.docEditor.wcTimerDoc.blockSignals(True) nwGUI.docEditor.wcTimerDoc.blockSignals(True)
nwGUI.docEditor.wcTimerSel.blockSignals(True) nwGUI.docEditor.wcTimerSel.blockSignals(True)
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
# Run on an empty document # Run on an empty document
nwGUI.docEditor._runDocCounter() nwGUI.docEditor._runDocCounter()
+35 -36
View File
@@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import pytest import pytest
from tools import C, cmpFiles, buildTestProject, XML_IGNORE, writeFile from tools import C, cmpFiles, buildTestProject, XML_IGNORE, writeFile
@@ -67,7 +66,7 @@ def testGuiMain_ProjectBlocker(nwGUI):
@pytest.mark.gui @pytest.mark.gui
def testGuiMain_NewProject(monkeypatch, nwGUI, fncProj): def testGuiMain_NewProject(monkeypatch, nwGUI, projPath):
"""Test creating a new project. """Test creating a new project.
""" """
# No data # No data
@@ -79,34 +78,34 @@ def testGuiMain_NewProject(monkeypatch, nwGUI, fncProj):
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
nwGUI.hasProject = True nwGUI.hasProject = True
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) 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 # No project path
assert nwGUI.newProject(projData={}) is False assert nwGUI.newProject(projData={}) is False
# Project file already exists # Project file already exists
projFile = os.path.join(fncProj, nwFiles.PROJ_FILE) projFile = projPath / nwFiles.PROJ_FILE
writeFile(projFile, "Stuff") writeFile(projFile, "Stuff")
assert nwGUI.newProject(projData={"projPath": fncProj}) is False assert nwGUI.newProject(projData={"projPath": projPath}) is False
os.unlink(projFile) projFile.unlink()
# An unreachable path should also fail # An unreachable path should also fail
projPath = os.path.join(fncProj, "stuff", "stuff", "stuff") stuffPath = projPath / "stuff" / "stuff" / "stuff"
assert nwGUI.newProject(projData={"projPath": projPath}) is False assert nwGUI.newProject(projData={"projPath": stuffPath}) is False
# This one should work just fine # This one should work just fine
assert nwGUI.newProject(projData={"projPath": fncProj}) is True assert nwGUI.newProject(projData={"projPath": projPath}) is True
assert os.path.isfile(os.path.join(fncProj, nwFiles.PROJ_FILE)) assert (projPath / nwFiles.PROJ_FILE).is_file()
assert os.path.isdir(os.path.join(fncProj, "content")) assert (projPath / "content").is_dir()
# END Test testGuiMain_NewProject # END Test testGuiMain_NewProject
@pytest.mark.gui @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. """Test handling of project tree items based on GUI focus states.
""" """
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
sHandle = "000000000000f" sHandle = "000000000000f"
assert nwGUI.openSelectedItem() is False assert nwGUI.openSelectedItem() is False
@@ -153,7 +152,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
@pytest.mark.gui @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. """Test the document editor.
""" """
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) 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)) monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
# Create new, save, close project # Create new, save, close project
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
assert nwGUI.saveProject() assert nwGUI.saveProject()
assert nwGUI.closeProject() assert nwGUI.closeProject()
@@ -176,14 +175,14 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
assert nwGUI.theProject.data.spellCheck is False assert nwGUI.theProject.data.spellCheck is False
# Check the files # Check the files
projFile = os.path.join(fncProj, "nwProject.nwx") projFile = projPath / "nwProject.nwx"
testFile = os.path.join(outDir, "guiEditor_Main_Initial_nwProject.nwx") testFile = tstPaths.outDir / "guiEditor_Main_Initial_nwProject.nwx"
compFile = os.path.join(refDir, "guiEditor_Main_Initial_nwProject.nwx") compFile = tstPaths.refDir / "guiEditor_Main_Initial_nwProject.nwx"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE) assert cmpFiles(testFile, compFile, ignoreStart=XML_IGNORE)
# Re-open project # Re-open project
assert nwGUI.openProject(fncProj) assert nwGUI.openProject(projPath)
# Check that we loaded the data # Check that we loaded the data
assert len(nwGUI.theProject.tree) == 8 assert len(nwGUI.theProject.tree) == 8
@@ -494,33 +493,33 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
assert nwGUI.saveProject() assert nwGUI.saveProject()
# Check the files # Check the files
projFile = os.path.join(fncProj, "nwProject.nwx") projFile = projPath / "nwProject.nwx"
testFile = os.path.join(outDir, "guiEditor_Main_Final_nwProject.nwx") testFile = tstPaths.outDir / "guiEditor_Main_Final_nwProject.nwx"
compFile = os.path.join(refDir, "guiEditor_Main_Final_nwProject.nwx") compFile = tstPaths.refDir / "guiEditor_Main_Final_nwProject.nwx"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, ignoreStart=(*XML_IGNORE, "<spellCheck")) assert cmpFiles(testFile, compFile, ignoreStart=(*XML_IGNORE, "<spellCheck"))
projFile = os.path.join(fncProj, "content", "000000000000f.nwd") projFile = projPath / "content" / "000000000000f.nwd"
testFile = os.path.join(outDir, "guiEditor_Main_Final_000000000000f.nwd") testFile = tstPaths.outDir / "guiEditor_Main_Final_000000000000f.nwd"
compFile = os.path.join(refDir, "guiEditor_Main_Final_000000000000f.nwd") compFile = tstPaths.refDir / "guiEditor_Main_Final_000000000000f.nwd"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile) assert cmpFiles(testFile, compFile)
projFile = os.path.join(fncProj, "content", "0000000000010.nwd") projFile = projPath / "content" / "0000000000010.nwd"
testFile = os.path.join(outDir, "guiEditor_Main_Final_0000000000010.nwd") testFile = tstPaths.outDir / "guiEditor_Main_Final_0000000000010.nwd"
compFile = os.path.join(refDir, "guiEditor_Main_Final_0000000000010.nwd") compFile = tstPaths.refDir / "guiEditor_Main_Final_0000000000010.nwd"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile) assert cmpFiles(testFile, compFile)
projFile = os.path.join(fncProj, "content", "0000000000011.nwd") projFile = projPath / "content" / "0000000000011.nwd"
testFile = os.path.join(outDir, "guiEditor_Main_Final_0000000000011.nwd") testFile = tstPaths.outDir / "guiEditor_Main_Final_0000000000011.nwd"
compFile = os.path.join(refDir, "guiEditor_Main_Final_0000000000011.nwd") compFile = tstPaths.refDir / "guiEditor_Main_Final_0000000000011.nwd"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile) assert cmpFiles(testFile, compFile)
projFile = os.path.join(fncProj, "content", "0000000000012.nwd") projFile = projPath / "content" / "0000000000012.nwd"
testFile = os.path.join(outDir, "guiEditor_Main_Final_0000000000012.nwd") testFile = tstPaths.outDir / "guiEditor_Main_Final_0000000000012.nwd"
compFile = os.path.join(refDir, "guiEditor_Main_Final_0000000000012.nwd") compFile = tstPaths.refDir / "guiEditor_Main_Final_0000000000012.nwd"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile) assert cmpFiles(testFile, compFile)
@@ -530,10 +529,10 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir, mock
@pytest.mark.gui @pytest.mark.gui
def testGuiMain_FocusFullMode(qtbot, nwGUI, fncProj, mockRnd): def testGuiMain_FocusFullMode(qtbot, nwGUI, projPath, mockRnd):
"""Test toggling focus mode in main window. """Test toggling focus mode in main window.
""" """
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
assert nwGUI.isFocusMode is False assert nwGUI.isFocusMode is False
# Focus Mode # Focus Mode
+6 -7
View File
@@ -20,10 +20,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import pytest import pytest
import os
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QTextCursor, QTextBlock from PyQt5.QtGui import QTextCursor, QTextBlock
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox
from tools import C, writeFile, buildTestProject from tools import C, writeFile, buildTestProject
@@ -422,10 +421,10 @@ def testGuiMenu_ContextMenus(qtbot, nwGUI, nwLipsum):
@pytest.mark.gui @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. """Test the Insert menu.
""" """
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
assert nwGUI.projView.projTree._getTreeItem(C.hSceneDoc) is not None assert nwGUI.projView.projTree._getTreeItem(C.hSceneDoc) is not None
assert nwGUI.openDocument(C.hSceneDoc) is True assert nwGUI.openDocument(C.hSceneDoc) is True
@@ -626,8 +625,8 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj, mockRnd):
assert not nwGUI.importDocument() assert not nwGUI.importDocument()
# Then a valid path, but bot a file that exists # Then a valid path, but bot a file that exists
theFile = os.path.join(fncDir, "import.txt") theFile = fncPath / "import.txt"
monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (theFile, "")) monkeypatch.setattr(QFileDialog, "getOpenFileName", lambda *a, **k: (str(theFile), ""))
assert not nwGUI.importDocument() assert not nwGUI.importDocument()
# Create the file and try again, but with no target document open # 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("<br>") theBits = theMessage.split("<br>")
assert len(theBits) == 2 assert len(theBits) == 2
assert theBits[0] == "The currently open file is saved in:" 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() # qtbot.stop()
+2 -2
View File
@@ -35,12 +35,12 @@ from novelwriter.dialogs.editlabel import GuiEditLabel
@pytest.mark.gui @pytest.mark.gui
def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd): def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test navigating the novel tree. """Test navigating the novel tree.
""" """
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
nwGUI.switchFocus(nwWidget.TREE) nwGUI.switchFocus(nwWidget.TREE)
nwGUI.projView.projTree.clearSelection() nwGUI.projView.projTree.clearSelection()
+2 -4
View File
@@ -32,12 +32,11 @@ from novelwriter.enum import nwItemClass, nwOutline, nwView
@pytest.mark.gui @pytest.mark.gui
def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir): def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, projPath):
"""Test the outline view. """Test the outline view.
""" """
# Create a project # Create a project
prjDir = os.path.join(fncDir, "project") buildTestProject(nwGUI, projPath)
buildTestProject(nwGUI, prjDir)
nwGUI.rebuildIndex() nwGUI.rebuildIndex()
nwGUI._changeView(nwView.OUTLINE) nwGUI._changeView(nwView.OUTLINE)
@@ -156,7 +155,6 @@ def testGuiOutline_Content(qtbot, nwGUI, nwLipsum):
"""Test the outline view. """Test the outline view.
""" """
assert nwGUI.openProject(nwLipsum) assert nwGUI.openProject(nwLipsum)
nwGUI.mainConf.lastPath = nwLipsum
nwGUI.rebuildIndex() nwGUI.rebuildIndex()
nwGUI._changeView(nwView.OUTLINE) nwGUI._changeView(nwView.OUTLINE)
+23 -34
View File
@@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import pytest import pytest
from mock import causeOSError from mock import causeOSError
@@ -36,7 +35,7 @@ from novelwriter.dialogs.editlabel import GuiEditLabel
@pytest.mark.gui @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. """Test adding and removing items from the project tree.
""" """
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) 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 assert projView.projTree.newTreeItem(nwItemType.FILE) is False
# Create a project # Create a project
prjDir = os.path.join(fncDir, "project") buildTestProject(nwGUI, projPath)
buildTestProject(nwGUI, prjDir)
# No itemType set # No itemType set
projView.projTree.clearSelection() projView.projTree.clearSelection()
@@ -168,7 +166,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd)
@pytest.mark.gui @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. """Test adding and removing items from the project tree.
""" """
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) 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 assert projView.projTree.moveTreeItem(1) is False
# Create a project # Create a project
prjDir = os.path.join(fncDir, "project") buildTestProject(nwGUI, projPath)
buildTestProject(nwGUI, prjDir)
# Move Documents # Move Documents
# ============== # ==============
@@ -279,7 +276,7 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
@pytest.mark.gui @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. """Test external requests for removing items from project tree.
""" """
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) 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 assert projView.requestDeleteItem() is False
# Create a project # Create a project
prjDir = os.path.join(fncDir, "project") buildTestProject(nwGUI, projPath)
buildTestProject(nwGUI, prjDir)
# Try emptying the trash already now, when there is no trash folder # Try emptying the trash already now, when there is no trash folder
assert projView.emptyTrash() is False assert projView.emptyTrash() is False
@@ -363,7 +359,7 @@ def testGuiProjTree_RequestDeleteItem(qtbot, caplog, monkeypatch, nwGUI, fncDir,
@pytest.mark.gui @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. """Test moving items to Trash.
""" """
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) 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 projTree = nwGUI.projView.projTree
# Create a project # Create a project
prjDir = os.path.join(fncDir, "project") buildTestProject(nwGUI, projPath)
buildTestProject(nwGUI, prjDir)
# Invalid item # Invalid item
caplog.clear() caplog.clear()
@@ -417,7 +412,7 @@ def testGuiProjTree_MoveItemToTrash(qtbot, caplog, monkeypatch, nwGUI, fncDir, m
@pytest.mark.gui @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. """Test permanently deleting items.
""" """
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) 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 projTree = nwGUI.projView.projTree
# Create a project # Create a project
prjDir = os.path.join(fncDir, "project") buildTestProject(nwGUI, projPath)
buildTestProject(nwGUI, prjDir)
# Invalid item # Invalid item
caplog.clear() caplog.clear()
@@ -470,7 +464,7 @@ def testGuiProjTree_PermanentlyDeleteItem(qtbot, caplog, monkeypatch, nwGUI, fnc
@pytest.mark.gui @pytest.mark.gui
def testGuiProjTree_EmptyTrash(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRnd): def testGuiProjTree_EmptyTrash(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
"""Test emptying Trash. """Test emptying Trash.
""" """
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) 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 assert "No project open" in caplog.text
# Create a project # Create a project
prjDir = os.path.join(fncDir, "project") buildTestProject(nwGUI, projPath)
buildTestProject(nwGUI, prjDir)
# No Trash folder # No Trash folder
assert projTree.emptyTrash() is False assert projTree.emptyTrash() is False
@@ -524,7 +517,7 @@ def testGuiProjTree_EmptyTrash(qtbot, caplog, monkeypatch, nwGUI, fncDir, mockRn
@pytest.mark.gui @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 """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, 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) monkeypatch.setattr(QMenu, "exec_", lambda *a: None)
# Create a project # Create a project
prjDir = os.path.join(fncDir, "project") buildTestProject(nwGUI, projPath)
buildTestProject(nwGUI, prjDir)
# Handles for new objects # Handles for new objects
hCharNote = "0000000000011" hCharNote = "0000000000011"
@@ -643,7 +635,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, fncDir, mockRnd):
@pytest.mark.gui @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. """Test the merge document function.
""" """
mergeData = {} mergeData = {}
@@ -654,8 +646,7 @@ def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, i
monkeypatch.setattr(GuiDocMerge, "getData", lambda *a: mergeData) monkeypatch.setattr(GuiDocMerge, "getData", lambda *a: mergeData)
# Create a project # Create a project
prjDir = os.path.join(fncDir, "project") buildTestProject(nwGUI, projPath)
buildTestProject(nwGUI, prjDir)
theProject = nwGUI.theProject theProject = nwGUI.theProject
projTree = nwGUI.projView.projTree projTree = nwGUI.projView.projTree
@@ -746,7 +737,7 @@ def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, i
@pytest.mark.gui @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. """Test the split document function.
""" """
splitData = {} splitData = {}
@@ -758,8 +749,7 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, ip
monkeypatch.setattr(GuiDocSplit, "getData", lambda *a: (splitData, splitText)) monkeypatch.setattr(GuiDocSplit, "getData", lambda *a: (splitData, splitText))
# Create a project # Create a project
prjDir = os.path.join(fncDir, "project") buildTestProject(nwGUI, projPath)
buildTestProject(nwGUI, prjDir)
theProject = nwGUI.theProject theProject = nwGUI.theProject
projTree = nwGUI.projView.projTree projTree = nwGUI.projView.projTree
@@ -828,13 +818,13 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, ip
assert projTree._splitDocument(hSplitDoc) is True assert projTree._splitDocument(hSplitDoc) is True
for tHandle in fstSet: for tHandle in fstSet:
assert tHandle in theProject.tree 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 # Writing succeeds
assert projTree._splitDocument(hSplitDoc) is True assert projTree._splitDocument(hSplitDoc) is True
for tHandle in sndSet: for tHandle in sndSet:
assert tHandle in theProject.tree 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 # Add to a folder and move source to trash
splitData["intoFolder"] = True splitData["intoFolder"] = True
@@ -843,7 +833,7 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, ip
assert "0000000000029" in theProject.tree # The folder assert "0000000000029" in theProject.tree # The folder
for tHandle in trdSet: for tHandle in trdSet:
assert tHandle in theProject.tree 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 assert theProject.tree.isTrash(hSplitDoc) is True
@@ -858,13 +848,12 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, fncDir, mockRnd, ip
@pytest.mark.gui @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 """Test various parts of the project tree class not covered by
other tests. other tests.
""" """
# Create a project # Create a project
prjDir = os.path.join(fncDir, "project") buildTestProject(nwGUI, projPath)
buildTestProject(nwGUI, prjDir)
projView = nwGUI.projView projView = nwGUI.projView
projTree = nwGUI.projView.projTree projTree = nwGUI.projView.projTree
+2 -2
View File
@@ -28,10 +28,10 @@ from novelwriter.enum import nwState
@pytest.mark.gui @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. """Test the the various features of the status bar.
""" """
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
cHandle = nwGUI.theProject.newFile("A Note", C.hCharRoot) cHandle = nwGUI.theProject.newFile("A Note", C.hCharRoot)
newDoc = nwGUI.theProject.storage.getDocument(cHandle) newDoc = nwGUI.theProject.storage.getDocument(cHandle)
newDoc.writeDocument("# A Note\n\n") newDoc.writeDocument("# A Note\n\n")
+22 -35
View File
@@ -19,10 +19,10 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import shutil import shutil
import pytest import pytest
from pathlib import Path
from configparser import ConfigParser from configparser import ConfigParser
from mock import causeOSError from mock import causeOSError
@@ -38,7 +38,7 @@ from novelwriter.gui.theme import GuiIcons, GuiTheme
@pytest.mark.gui @pytest.mark.gui
def testGuiTheme_Main(qtbot, nwGUI, fncDir): def testGuiTheme_Main(qtbot, nwGUI, fncPath):
"""Test the theme class init. """Test the theme class init.
""" """
mainTheme: GuiTheme = nwGUI.mainTheme mainTheme: GuiTheme = nwGUI.mainTheme
@@ -75,15 +75,15 @@ def testGuiTheme_Main(qtbot, nwGUI, fncDir):
# Scan for Themes # 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") themeOne = fncPath / "themes" / "themeone.conf"
themeTwo = os.path.join(fncDir, "themes", "themetwo.conf") themeTwo = fncPath / "themes" / "themetwo.conf"
writeFile(themeOne, "# Stuff") writeFile(themeOne, "# Stuff")
writeFile(themeTwo, "# Stuff") writeFile(themeTwo, "# Stuff")
result = {} 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["themeone"] == themeOne
assert result["themetwo"] == themeTwo assert result["themetwo"] == themeTwo
@@ -123,7 +123,7 @@ def testGuiTheme_Main(qtbot, nwGUI, fncDir):
@pytest.mark.gui @pytest.mark.gui
def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, fncDir): def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, fncPath):
"""Test the theme part of the class. """Test the theme part of the class.
""" """
mainTheme: GuiTheme = nwGUI.mainTheme mainTheme: GuiTheme = nwGUI.mainTheme
@@ -132,15 +132,8 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, fncDir):
# List Themes # List Themes
# =========== # ===========
shutil.copy( shutil.copy(mainConf.assetPath("themes") / "default_dark.conf", fncPath / "themes")
os.path.join(mainConf.assetPath, "themes", "default_dark.conf"), shutil.copy(mainConf.assetPath("themes") / "default.conf", fncPath / "themes")
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 */")
# Block the reading of the files # Block the reading of the files
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
@@ -197,7 +190,7 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, fncDir):
@pytest.mark.gui @pytest.mark.gui
def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI, fncDir): def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI, fncPath):
"""Test the syntax part of the class. """Test the syntax part of the class.
""" """
mainTheme: GuiTheme = nwGUI.mainTheme mainTheme: GuiTheme = nwGUI.mainTheme
@@ -206,14 +199,8 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI, fncDir):
# List Themes # List Themes
# =========== # ===========
shutil.copy( shutil.copy(mainConf.assetPath("syntax") / "default_dark.conf", fncPath / "syntax")
os.path.join(mainConf.assetPath, "syntax", "default_dark.conf"), shutil.copy(mainConf.assetPath("syntax") / "default_light.conf", fncPath / "syntax")
os.path.join(fncDir, "syntax")
)
shutil.copy(
os.path.join(mainConf.assetPath, "syntax", "default_light.conf"),
os.path.join(fncDir, "syntax")
)
# Block the reading of the files # Block the reading of the files
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
@@ -270,11 +257,10 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI, fncDir):
@pytest.mark.gui @pytest.mark.gui
def testGuiTheme_Icons(qtbot, caplog, monkeypatch, nwGUI, fncDir): def testGuiTheme_Icons(qtbot, caplog, monkeypatch, nwGUI, fncPath):
"""Test the icon cache class. """Test the icon cache class.
""" """
iconCache: GuiIcons = nwGUI.mainTheme.iconCache iconCache: GuiIcons = nwGUI.mainTheme.iconCache
mainConf: Config = nwGUI.mainConf
# Load Theme # Load Theme
# ========== # ==========
@@ -288,10 +274,11 @@ def testGuiTheme_Icons(qtbot, caplog, monkeypatch, nwGUI, fncDir):
assert iconCache.loadTheme("typicons_dark") is False assert iconCache.loadTheme("typicons_dark") is False
# Load a broken theme file # Load a broken theme file
iconsDir = os.path.join(fncDir, "icons") iconsDir = fncPath / "icons"
os.mkdir(iconsDir) testIcons = iconsDir / "testicons"
os.mkdir(os.path.join(iconsDir, "testicons")) iconsDir.mkdir()
writeFile(os.path.join(iconsDir, "testicons", "icons.conf"), ( testIcons.mkdir()
writeFile(testIcons / "icons.conf", (
"[Main]\n" "[Main]\n"
"name = Test Icons\n" "name = Test Icons\n"
"\n" "\n"
@@ -300,15 +287,15 @@ def testGuiTheme_Icons(qtbot, caplog, monkeypatch, nwGUI, fncDir):
"stuff = stuff.svg\n" "stuff = stuff.svg\n"
)) ))
assetPath = mainConf.assetPath iconPath = iconCache._iconPath
mainConf.assetPath = fncDir iconCache._iconPath = fncPath / "icons"
caplog.clear() caplog.clear()
assert iconCache.loadTheme("testicons") is True assert iconCache.loadTheme("testicons") is True
assert "Unknown icon name 'stuff' in config file" in caplog.text assert "Unknown icon name 'stuff' in config file" in caplog.text
assert "Icon file 'add.svg' not in theme folder" 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 # Load working theme file
assert iconCache.loadTheme("typicons_dark") is True assert iconCache.loadTheme("typicons_dark") is True
@@ -327,7 +314,7 @@ def testGuiTheme_Icons(qtbot, caplog, monkeypatch, nwGUI, fncDir):
# Fail finding the file # Fail finding the file
with monkeypatch.context() as mp: 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") qPix = iconCache.loadDecoration("wiz-back")
assert qPix.isNull() is True assert qPix.isNull() is True
+57 -65
View File
@@ -20,9 +20,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import pytest import pytest
import os
from shutil import copyfile from shutil import copyfile
from tools import cmpFiles, getGuiItem from tools import cmpFiles, getGuiItem
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
@@ -32,7 +32,7 @@ from novelwriter.tools import GuiBuildNovel
@pytest.mark.gui @pytest.mark.gui
def testToolBuild_Main(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir): def testToolBuild_Main(qtbot, monkeypatch, nwGUI, prjLipsum, tstPaths):
"""Test the build tool. """Test the build tool.
""" """
# Block message box # Block message box
@@ -43,7 +43,7 @@ def testToolBuild_Main(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir):
assert getGuiItem("GuiBuildNovel") is None assert getGuiItem("GuiBuildNovel") is None
# Open a project # Open a project
assert nwGUI.openProject(nwLipsum) assert nwGUI.openProject(prjLipsum)
# Open the tool # Open the tool
nwGUI.mainMenu.aBuildProject.activate(QAction.Trigger) nwGUI.mainMenu.aBuildProject.activate(QAction.Trigger)
@@ -61,55 +61,47 @@ def testToolBuild_Main(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir):
# Invalid file format # Invalid file format
assert not nwBuild._saveDocument(-1) 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 # No path selected
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QFileDialog, "getSaveFileName", lambda *a, **k: ("", "")) mp.setattr(QFileDialog, "getSaveFileName", lambda *a, **k: ("", ""))
assert not nwBuild._saveDocument(nwBuild.FMT_NWD) assert not nwBuild._saveDocument(nwBuild.FMT_NWD)
# Default Settings # Default Settings
nwGUI.mainConf.lastPath = nwLipsum nwGUI.mainConf._lastPath = prjLipsum
qtbot.mouseClick(nwBuild.buildNovel, Qt.LeftButton) qtbot.mouseClick(nwBuild.buildNovel, Qt.LeftButton)
assert nwBuild._saveDocument(nwBuild.FMT_NWD) assert nwBuild._saveDocument(nwBuild.FMT_NWD)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd") projFile = prjLipsum / "Lorem Ipsum.nwd"
testFile = os.path.join(outDir, "guiBuild_Tool_Step1_Lorem_Ipsum.nwd") testFile = tstPaths.outDir / "guiBuild_Tool_Step1_Lorem_Ipsum.nwd"
compFile = os.path.join(refDir, "guiBuild_Tool_Step1_Lorem_Ipsum.nwd") compFile = tstPaths.refDir / "guiBuild_Tool_Step1_Lorem_Ipsum.nwd"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile) assert cmpFiles(testFile, compFile)
assert nwBuild._saveDocument(nwBuild.FMT_HTM) assert nwBuild._saveDocument(nwBuild.FMT_HTM)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm") projFile = prjLipsum / "Lorem Ipsum.htm"
testFile = os.path.join(outDir, "guiBuild_Tool_Step1_Lorem_Ipsum.htm") testFile = tstPaths.outDir / "guiBuild_Tool_Step1_Lorem_Ipsum.htm"
compFile = os.path.join(refDir, "guiBuild_Tool_Step1_Lorem_Ipsum.htm") compFile = tstPaths.refDir / "guiBuild_Tool_Step1_Lorem_Ipsum.htm"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile) assert cmpFiles(testFile, compFile)
assert nwBuild._saveDocument(nwBuild.FMT_MD) assert nwBuild._saveDocument(nwBuild.FMT_MD)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.md") projFile = prjLipsum / "Lorem Ipsum.md"
testFile = os.path.join(outDir, "guiBuild_Tool_Step1_Lorem_Ipsum.md") testFile = tstPaths.outDir / "guiBuild_Tool_Step1_Lorem_Ipsum.md"
compFile = os.path.join(refDir, "guiBuild_Tool_Step1_Lorem_Ipsum.md") compFile = tstPaths.refDir / "guiBuild_Tool_Step1_Lorem_Ipsum.md"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile) assert cmpFiles(testFile, compFile)
assert nwBuild._saveDocument(nwBuild.FMT_GH) assert nwBuild._saveDocument(nwBuild.FMT_GH)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.md") projFile = prjLipsum / "Lorem Ipsum.md"
testFile = os.path.join(outDir, "guiBuild_Tool_Step1G_Lorem_Ipsum.md") testFile = tstPaths.outDir / "guiBuild_Tool_Step1G_Lorem_Ipsum.md"
compFile = os.path.join(refDir, "guiBuild_Tool_Step1G_Lorem_Ipsum.md") compFile = tstPaths.refDir / "guiBuild_Tool_Step1G_Lorem_Ipsum.md"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile) assert cmpFiles(testFile, compFile)
assert nwBuild._saveDocument(nwBuild.FMT_FODT) assert nwBuild._saveDocument(nwBuild.FMT_FODT)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.fodt") projFile = prjLipsum / "Lorem Ipsum.fodt"
testFile = os.path.join(outDir, "guiBuild_Tool_Step1_Lorem_Ipsum.fodt") testFile = tstPaths.outDir / "guiBuild_Tool_Step1_Lorem_Ipsum.fodt"
compFile = os.path.join(refDir, "guiBuild_Tool_Step1_Lorem_Ipsum.fodt") compFile = tstPaths.refDir / "guiBuild_Tool_Step1_Lorem_Ipsum.fodt"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [4, 5]) assert cmpFiles(testFile, compFile, [4, 5])
@@ -130,30 +122,30 @@ def testToolBuild_Main(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir):
qtbot.mouseClick(nwBuild.buildNovel, Qt.LeftButton) qtbot.mouseClick(nwBuild.buildNovel, Qt.LeftButton)
assert nwBuild._saveDocument(nwBuild.FMT_NWD) assert nwBuild._saveDocument(nwBuild.FMT_NWD)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd") projFile = prjLipsum / "Lorem Ipsum.nwd"
testFile = os.path.join(outDir, "guiBuild_Tool_Step2_Lorem_Ipsum.nwd") testFile = tstPaths.outDir / "guiBuild_Tool_Step2_Lorem_Ipsum.nwd"
compFile = os.path.join(refDir, "guiBuild_Tool_Step2_Lorem_Ipsum.nwd") compFile = tstPaths.refDir / "guiBuild_Tool_Step2_Lorem_Ipsum.nwd"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile) assert cmpFiles(testFile, compFile)
assert nwBuild._saveDocument(nwBuild.FMT_HTM) assert nwBuild._saveDocument(nwBuild.FMT_HTM)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm") projFile = prjLipsum / "Lorem Ipsum.htm"
testFile = os.path.join(outDir, "guiBuild_Tool_Step2_Lorem_Ipsum.htm") testFile = tstPaths.outDir / "guiBuild_Tool_Step2_Lorem_Ipsum.htm"
compFile = os.path.join(refDir, "guiBuild_Tool_Step2_Lorem_Ipsum.htm") compFile = tstPaths.refDir / "guiBuild_Tool_Step2_Lorem_Ipsum.htm"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile) assert cmpFiles(testFile, compFile)
assert nwBuild._saveDocument(nwBuild.FMT_MD) assert nwBuild._saveDocument(nwBuild.FMT_MD)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.md") projFile = prjLipsum / "Lorem Ipsum.md"
testFile = os.path.join(outDir, "guiBuild_Tool_Step2_Lorem_Ipsum.md") testFile = tstPaths.outDir / "guiBuild_Tool_Step2_Lorem_Ipsum.md"
compFile = os.path.join(refDir, "guiBuild_Tool_Step2_Lorem_Ipsum.md") compFile = tstPaths.refDir / "guiBuild_Tool_Step2_Lorem_Ipsum.md"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile) assert cmpFiles(testFile, compFile)
assert nwBuild._saveDocument(nwBuild.FMT_FODT) assert nwBuild._saveDocument(nwBuild.FMT_FODT)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.fodt") projFile = prjLipsum / "Lorem Ipsum.fodt"
testFile = os.path.join(outDir, "guiBuild_Tool_Step2_Lorem_Ipsum.fodt") testFile = tstPaths.outDir / "guiBuild_Tool_Step2_Lorem_Ipsum.fodt"
compFile = os.path.join(refDir, "guiBuild_Tool_Step2_Lorem_Ipsum.fodt") compFile = tstPaths.refDir / "guiBuild_Tool_Step2_Lorem_Ipsum.fodt"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [4, 5]) assert cmpFiles(testFile, compFile, [4, 5])
@@ -164,30 +156,30 @@ def testToolBuild_Main(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir):
# Save files that can be compared # Save files that can be compared
assert nwBuild._saveDocument(nwBuild.FMT_NWD) assert nwBuild._saveDocument(nwBuild.FMT_NWD)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd") projFile = prjLipsum / "Lorem Ipsum.nwd"
testFile = os.path.join(outDir, "guiBuild_Tool_Step3_Lorem_Ipsum.nwd") testFile = tstPaths.outDir / "guiBuild_Tool_Step3_Lorem_Ipsum.nwd"
compFile = os.path.join(refDir, "guiBuild_Tool_Step3_Lorem_Ipsum.nwd") compFile = tstPaths.refDir / "guiBuild_Tool_Step3_Lorem_Ipsum.nwd"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile) assert cmpFiles(testFile, compFile)
assert nwBuild._saveDocument(nwBuild.FMT_HTM) assert nwBuild._saveDocument(nwBuild.FMT_HTM)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm") projFile = prjLipsum / "Lorem Ipsum.htm"
testFile = os.path.join(outDir, "guiBuild_Tool_Step3_Lorem_Ipsum.htm") testFile = tstPaths.outDir / "guiBuild_Tool_Step3_Lorem_Ipsum.htm"
compFile = os.path.join(refDir, "guiBuild_Tool_Step3_Lorem_Ipsum.htm") compFile = tstPaths.refDir / "guiBuild_Tool_Step3_Lorem_Ipsum.htm"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile) assert cmpFiles(testFile, compFile)
assert nwBuild._saveDocument(nwBuild.FMT_MD) assert nwBuild._saveDocument(nwBuild.FMT_MD)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.md") projFile = prjLipsum / "Lorem Ipsum.md"
testFile = os.path.join(outDir, "guiBuild_Tool_Step3_Lorem_Ipsum.md") testFile = tstPaths.outDir / "guiBuild_Tool_Step3_Lorem_Ipsum.md"
compFile = os.path.join(refDir, "guiBuild_Tool_Step3_Lorem_Ipsum.md") compFile = tstPaths.refDir / "guiBuild_Tool_Step3_Lorem_Ipsum.md"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile) assert cmpFiles(testFile, compFile)
assert nwBuild._saveDocument(nwBuild.FMT_FODT) assert nwBuild._saveDocument(nwBuild.FMT_FODT)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.fodt") projFile = prjLipsum / "Lorem Ipsum.fodt"
testFile = os.path.join(outDir, "guiBuild_Tool_Step3_Lorem_Ipsum.fodt") testFile = tstPaths.outDir / "guiBuild_Tool_Step3_Lorem_Ipsum.fodt"
compFile = os.path.join(refDir, "guiBuild_Tool_Step3_Lorem_Ipsum.fodt") compFile = tstPaths.refDir / "guiBuild_Tool_Step3_Lorem_Ipsum.fodt"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [4, 5]) assert cmpFiles(testFile, compFile, [4, 5])
@@ -205,43 +197,43 @@ def testToolBuild_Main(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir):
# Save files that can be compared # Save files that can be compared
assert nwBuild._saveDocument(nwBuild.FMT_NWD) assert nwBuild._saveDocument(nwBuild.FMT_NWD)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.nwd") projFile = prjLipsum / "Lorem Ipsum.nwd"
testFile = os.path.join(outDir, "guiBuild_Tool_Step4_Lorem_Ipsum.nwd") testFile = tstPaths.outDir / "guiBuild_Tool_Step4_Lorem_Ipsum.nwd"
compFile = os.path.join(refDir, "guiBuild_Tool_Step4_Lorem_Ipsum.nwd") compFile = tstPaths.refDir / "guiBuild_Tool_Step4_Lorem_Ipsum.nwd"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile) assert cmpFiles(testFile, compFile)
assert nwBuild._saveDocument(nwBuild.FMT_HTM) assert nwBuild._saveDocument(nwBuild.FMT_HTM)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.htm") projFile = prjLipsum / "Lorem Ipsum.htm"
testFile = os.path.join(outDir, "guiBuild_Tool_Step4_Lorem_Ipsum.htm") testFile = tstPaths.outDir / "guiBuild_Tool_Step4_Lorem_Ipsum.htm"
compFile = os.path.join(refDir, "guiBuild_Tool_Step4_Lorem_Ipsum.htm") compFile = tstPaths.refDir / "guiBuild_Tool_Step4_Lorem_Ipsum.htm"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile) assert cmpFiles(testFile, compFile)
# Check the JSON files too at this stage # Check the JSON files too at this stage
assert nwBuild._saveDocument(nwBuild.FMT_JSON_H) assert nwBuild._saveDocument(nwBuild.FMT_JSON_H)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.json") projFile = prjLipsum / "Lorem Ipsum.json"
testFile = os.path.join(outDir, "guiBuild_Tool_Step4H_Lorem_Ipsum.json") testFile = tstPaths.outDir / "guiBuild_Tool_Step4H_Lorem_Ipsum.json"
compFile = os.path.join(refDir, "guiBuild_Tool_Step4H_Lorem_Ipsum.json") compFile = tstPaths.refDir / "guiBuild_Tool_Step4H_Lorem_Ipsum.json"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [8]) assert cmpFiles(testFile, compFile, [8])
assert nwBuild._saveDocument(nwBuild.FMT_JSON_M) assert nwBuild._saveDocument(nwBuild.FMT_JSON_M)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.json") projFile = prjLipsum / "Lorem Ipsum.json"
testFile = os.path.join(outDir, "guiBuild_Tool_Step4M_Lorem_Ipsum.json") testFile = tstPaths.outDir / "guiBuild_Tool_Step4M_Lorem_Ipsum.json"
compFile = os.path.join(refDir, "guiBuild_Tool_Step4M_Lorem_Ipsum.json") compFile = tstPaths.refDir / "guiBuild_Tool_Step4M_Lorem_Ipsum.json"
copyfile(projFile, testFile) copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile, [8]) assert cmpFiles(testFile, compFile, [8])
# Since odt and fodt is built by the same code, we don't check the # 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 # output. but just that the different format can be written as well
assert nwBuild._saveDocument(nwBuild.FMT_ODT) 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 # Print to PDF
if not nwGUI.mainConf.osDarwin: if not nwGUI.mainConf.osDarwin:
assert nwBuild._saveDocument(nwBuild.FMT_PDF) 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 # Close the build tool
htmlText = nwBuild.htmlText htmlText = nwBuild.htmlText
+2 -2
View File
@@ -29,7 +29,7 @@ from novelwriter.tools import GuiLipsum
@pytest.mark.gui @pytest.mark.gui
def testToolLipsum_Main(qtbot, nwGUI, fncProj, mockRnd): def testToolLipsum_Main(qtbot, nwGUI, projPath, mockRnd):
"""Test the Lorem Ipsum tool. """Test the Lorem Ipsum tool.
""" """
# Check that we cannot open when there is no project # 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 assert getGuiItem("GuiLipsum") is None
# Create a new project # Create a new project
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
assert nwGUI.openDocument(C.hSceneDoc) is True assert nwGUI.openDocument(C.hSceneDoc) is True
assert len(nwGUI.docEditor.getText()) == 15 assert len(nwGUI.docEditor.getText()) == 15
+10 -13
View File
@@ -19,7 +19,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import sys import sys
import pytest import pytest
@@ -37,7 +36,7 @@ from novelwriter.tools.projwizard import (
@pytest.mark.gui @pytest.mark.gui
@pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin") @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. """Test the launch of the project wizard.
Disabled for macOS because the test segfaults on QWizard.show() 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 # New with a project open should cause an error
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(nwGUI, "closeProject", lambda *a: False) mp.setattr(nwGUI, "closeProject", lambda *a: False)
assert nwGUI.newProject() is False assert nwGUI.newProject() is False
@@ -61,13 +60,12 @@ def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, fncProj):
assert nwGUI.newProject() is False assert nwGUI.newProject() is False
# Now, with a non-empty folder # 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 assert nwGUI.newProject() is False
# Test the Wizard Launching # Test the Wizard Launching
# ========================= # =========================
nwGUI.mainConf.lastPath = " "
monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *a: None) monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *a: None)
result = nwGUI.showNewProjectDialog() result = nwGUI.showNewProjectDialog()
@@ -97,12 +95,11 @@ def testToolProjectWizard_Handling(qtbot, monkeypatch, nwGUI, fncProj):
@pytest.mark.gui @pytest.mark.gui
@pytest.mark.parametrize("prjType", ["minimal", "custom1", "custom2", "sample"]) @pytest.mark.parametrize("prjType", ["minimal", "custom1", "custom2", "sample"])
@pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin") @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. """Test the new project wizard with a set of selection scenarios.
""" """
monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *a: None) monkeypatch.setattr(GuiProjectWizard, "exec_", lambda *a: None)
nwGUI.mainConf.lastPath = " "
nwWiz = GuiProjectWizard(nwGUI) nwWiz = GuiProjectWizard(nwGUI)
nwWiz.show() nwWiz.show()
qtbot.addWidget(nwWiz) qtbot.addWidget(nwWiz)
@@ -132,12 +129,12 @@ def testToolProjectWizard_Run(qtbot, monkeypatch, nwGUI, fncDir, prjType):
assert storagePage.errLabel.text() == "" assert storagePage.errLabel.text() == ""
# Set an invalid path # 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 not nwWiz.button(QWizard.NextButton).isEnabled()
assert storagePage.errLabel.text().startswith("Error") assert storagePage.errLabel.text().startswith("Error")
# Set an existing path # Set an existing path
storagePage.projPath.setText(fncDir) storagePage.projPath.setText(str(fncPath))
assert not nwWiz.button(QWizard.NextButton).isEnabled() assert not nwWiz.button(QWizard.NextButton).isEnabled()
assert storagePage.errLabel.text().startswith("Error") assert storagePage.errLabel.text().startswith("Error")
@@ -148,12 +145,12 @@ def testToolProjectWizard_Run(qtbot, monkeypatch, nwGUI, fncDir, prjType):
assert storagePage.errLabel.text() == "" assert storagePage.errLabel.text() == ""
# Let the browse feature handle it # Let the browse feature handle it
projPath = os.path.join(fncDir, "Test Wizard") projPath = fncPath / "Test Wizard"
with monkeypatch.context() as mp: 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) qtbot.mouseClick(storagePage.browseButton, Qt.LeftButton, delay=100)
assert storagePage.projPath.text() == projPath assert storagePage.projPath.text() == str(projPath)
assert storagePage.errLabel.text() == "" assert storagePage.errLabel.text() == ""
# Setting projPath should activate the button # Setting projPath should activate the button
@@ -218,7 +215,7 @@ def testToolProjectWizard_Run(qtbot, monkeypatch, nwGUI, fncDir, prjType):
assert projData["projName"] == "Test Wizard" assert projData["projName"] == "Test Wizard"
assert projData["projTitle"] == "My Novel" assert projData["projTitle"] == "My Novel"
assert projData["projAuthors"] == "Jane Doe" assert projData["projAuthors"] == "Jane Doe"
assert projData["projPath"] == projPath assert projData["projPath"] == str(projPath)
assert projData["popMinimal"] == prjType.startswith("minimal") assert projData["popMinimal"] == prjType.startswith("minimal")
assert projData["popCustom"] == prjType.startswith("custom") assert projData["popCustom"] == prjType.startswith("custom")
assert projData["popSample"] == prjType.startswith("sample") assert projData["popSample"] == prjType.startswith("sample")
+12 -18
View File
@@ -19,9 +19,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import pytest
import json import json
import os import pytest
from mock import causeOSError from mock import causeOSError
from tools import getGuiItem, writeFile, buildTestProject from tools import getGuiItem, writeFile, buildTestProject
@@ -34,17 +33,16 @@ from novelwriter.constants import nwFiles
@pytest.mark.gui @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. """Test the full writing stats tool.
""" """
# Create a project to work on # Create a project to work on
buildTestProject(nwGUI, fncProj) buildTestProject(nwGUI, projPath)
qtbot.wait(100) qtbot.wait(100)
assert nwGUI.saveProject() assert nwGUI.saveProject()
sessFile = os.path.join(fncProj, "meta", nwFiles.SESS_STATS) sessFile = projPath / "meta" / nwFiles.SESS_STATS
# Open the Writing Stats dialog # Open the Writing Stats dialog
nwGUI.mainConf.lastPath = ""
nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger) nwGUI.mainMenu.aWritingStats.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiWritingStats") is not None, timeout=1000) qtbot.waitUntil(lambda: getGuiItem("GuiWritingStats") is not None, timeout=1000)
@@ -55,7 +53,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
# ============ # ============
# No initial logfile # No initial logfile
assert not os.path.isfile(sessFile) assert not sessFile.is_file()
assert not sessLog._loadLogFile() assert not sessLog._loadLogFile()
# Make a test log file # Make a test log file
@@ -67,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-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" "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._loadLogFile()
assert sessLog.wordOffset == 123 assert sessLog.wordOffset == 123
assert len(sessLog.logData) == 4 assert len(sessLog.logData) == 4
@@ -111,9 +109,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
assert not sessLog._saveData(None) assert not sessLog._saveData(None)
# Make the save succeed # Make the save succeed
monkeypatch.setattr("os.path.expanduser", lambda *a: fncDir)
monkeypatch.setattr(QFileDialog, "getSaveFileName", lambda ss, tt, pp, options: (pp, "")) monkeypatch.setattr(QFileDialog, "getSaveFileName", lambda ss, tt, pp, options: (pp, ""))
sessLog.listBox.sortByColumn(sessLog.C_TIME, 0) sessLog.listBox.sortByColumn(sessLog.C_TIME, 0)
assert sessLog.novelWords.text() == "{:n}".format(600) assert sessLog.novelWords.text() == "{:n}".format(600)
@@ -135,10 +131,8 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
assert sessLog._saveData(sessLog.FMT_JSON) assert sessLog._saveData(sessLog.FMT_JSON)
qtbot.wait(100) qtbot.wait(100)
assert nwGUI.mainConf.lastPath == fncDir
# Check the exported files # 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: with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.load(inFile) jsonData = json.load(inFile)
@@ -177,7 +171,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
qtbot.mouseClick(sessLog.incNovel, Qt.LeftButton) qtbot.mouseClick(sessLog.incNovel, Qt.LeftButton)
assert sessLog._saveData(sessLog.FMT_JSON) 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: with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.loads(inFile.read()) jsonData = json.loads(inFile.read())
@@ -223,7 +217,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
qtbot.mouseClick(sessLog.incNotes, Qt.LeftButton) qtbot.mouseClick(sessLog.incNotes, Qt.LeftButton)
assert sessLog._saveData(sessLog.FMT_JSON) 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: with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.load(inFile) jsonData = json.load(inFile)
@@ -271,7 +265,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
# qtbot.stop() # qtbot.stop()
jsonStats = os.path.join(fncDir, "sessionStats.json") jsonStats = fncPath / "sessionStats.json"
with open(jsonStats, mode="r", encoding="utf-8") as inFile: with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.load(inFile) jsonData = json.load(inFile)
@@ -301,7 +295,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
qtbot.mouseClick(sessLog.hideZeros, Qt.LeftButton) qtbot.mouseClick(sessLog.hideZeros, Qt.LeftButton)
assert sessLog._saveData(sessLog.FMT_JSON) 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: with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.load(inFile) jsonData = json.load(inFile)
@@ -354,7 +348,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
qtbot.mouseClick(sessLog.groupByDay, Qt.LeftButton) qtbot.mouseClick(sessLog.groupByDay, Qt.LeftButton)
assert sessLog._saveData(sessLog.FMT_JSON) 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: with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.load(inFile) jsonData = json.load(inFile)
+14 -12
View File
@@ -19,10 +19,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import time import time
import shutil import shutil
from pathlib import Path
from PyQt5.QtWidgets import qApp from PyQt5.QtWidgets import qApp
XML_IGNORE = ("<novelWriterXML", "<project") XML_IGNORE = ("<novelWriterXML", "<project")
@@ -129,24 +130,25 @@ def writeFile(fileName, fileData):
outFile.write(fileData) outFile.write(fileData)
def cleanProject(projPath): def cleanProject(path):
"""Delete all generated files in a project. """Delete all generated files in a project.
""" """
cacheDir = os.path.join(projPath, "cache") path = Path(path)
if os.path.isdir(cacheDir): cacheDir = path / "cache"
if cacheDir.is_dir():
shutil.rmtree(cacheDir) shutil.rmtree(cacheDir)
metaDir = os.path.join(projPath, "meta") metaDir = path / "meta"
if os.path.isdir(metaDir): if metaDir.is_dir():
shutil.rmtree(metaDir) shutil.rmtree(metaDir)
bakFile = os.path.join(projPath, "nwProject.bak") bakFile = path / "nwProject.bak"
if os.path.isfile(bakFile): if bakFile.is_file():
os.unlink(bakFile) bakFile.unlink()
tocFile = os.path.join(projPath, "ToC.txt") tocFile = path / "ToC.txt"
if os.path.isfile(tocFile): if tocFile.is_file():
os.unlink(tocFile) tocFile.unlink()
return return