diff --git a/docs/source/conf.py b/docs/source/conf.py index e7446bba..b253da66 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -28,7 +28,6 @@ version = "1.5" # The full version, including alpha/beta/rc tags release = "1.5-alpha0" - # -- General configuration --------------------------------------------------- os.environ["TZ"] = "Europe/Oslo" diff --git a/nw/common.py b/nw/common.py index daa2b32d..c705922c 100644 --- a/nw/common.py +++ b/nw/common.py @@ -26,24 +26,27 @@ along with this program. If not, see . import logging from datetime import datetime +from configparser import ConfigParser from PyQt5.QtWidgets import qApp from PyQt5.QtCore import QCoreApplication from nw.enum import nwItemClass, nwItemType, nwItemLayout +from nw.error import logException from nw.constants import nwConst, nwUnicode logger = logging.getLogger(__name__) +# =============================================================================================== # +# Checker Functions +# =============================================================================================== # + def checkString(value, default, allowNone=False): """Check if a variable is a string or a none. """ - if allowNone: - if value is None: - return None - if value == "None": - return None + if allowNone and (value is None or value == "None"): + return None if isinstance(value, str): return str(value) return default @@ -52,11 +55,8 @@ def checkString(value, default, allowNone=False): def checkInt(value, default, allowNone=False): """Check if a variable is an integer or a none. """ - if allowNone: - if value is None: - return None - if value == "None": - return None + if allowNone and (value is None or value == "None"): + return None try: return int(value) except Exception: @@ -66,11 +66,9 @@ def checkInt(value, default, allowNone=False): def checkBool(value, default, allowNone=False): """Check if a variable is a boolean or a none. """ - if allowNone: - if value is None: - return None - if value == "None": - return None + if allowNone and (value is None or value == "None"): + return None + if isinstance(value, str): if value == "True": return True @@ -78,6 +76,7 @@ def checkBool(value, default, allowNone=False): return False else: return default + elif isinstance(value, int): if value == 1: return True @@ -85,67 +84,69 @@ def checkBool(value, default, allowNone=False): return False else: return default + return default def checkHandle(value, default, allowNone=False): """Check if a value is a handle. """ - if allowNone: - if value is None: - return None - if value == "None": - return None + if allowNone and (value is None or value == "None"): + return None if isHandle(value): return str(value) return default -def isHandle(theString): +# =============================================================================================== # +# Validator Functions +# =============================================================================================== # + +def isHandle(value): """Check if a string is a valid novelWriter handle. Note: This is case sensitive. Must be lower case! """ - if not isinstance(theString, str): + if not isinstance(value, str): return False - if len(theString) != 13: + if len(value) != 13: return False - for c in theString: + for c in value: if c not in "0123456789abcdef": return False return True -def isTitleTag(theString): +def isTitleTag(value): """Check if a string is a valid title string. """ - if not isinstance(theString, str): + if not isinstance(value, str): return False - if len(theString) != 7: + if len(value) != 7: return False - if not theString.startswith("T"): + if not value.startswith("T"): return False - for c in theString[1:]: + for c in value[1:]: if c not in "0123456789": return False return True -def isItemClass(theString): - """Check if an item is a calid nwItemClass identifier. +def isItemClass(value): + """Check if a string is a valid nwItemClass identifier. """ - return theString in nwItemClass.__members__ + return value in nwItemClass.__members__ -def isItemType(theString): - """Check if an item is a calid nwItemType identifier. +def isItemType(value): + """Check if a string is a valid nwItemType identifier. """ - return theString in nwItemType.__members__ + return value in nwItemType.__members__ -def isItemLayout(theString): - """Check if an item is a calid nwItemLayout identifier. +def isItemLayout(value): + """Check if a string is a valid nwItemLayout identifier. """ - return theString in nwItemLayout.__members__ + return value in nwItemLayout.__members__ def hexToInt(value, default=0): @@ -159,14 +160,19 @@ def hexToInt(value, default=0): return default -def formatInt(theInt): +# =============================================================================================== # +# Formatting Functions +# =============================================================================================== # + +def formatInt(value): """Formats an integer with k, M, G etc. """ - postFix = ["k", "M", "G", "T", "P", "E"] - theVal = float(theInt) + if not isinstance(value, int): + return "ERR" + theVal = float(value) if theVal > 1000.0: - for pF in postFix: + for pF in ["k", "M", "G", "T", "P", "E"]: theVal /= 1000.0 if theVal < 1000.0: if theVal < 10.0: @@ -176,7 +182,7 @@ def formatInt(theInt): else: return f"{theVal:3.0f}{nwUnicode.U_THSP}{pF}" - return str(theInt) + return str(value) def formatTimeStamp(theTime, fileSafe=False): @@ -201,6 +207,23 @@ def formatTime(tS): return "ERROR" +def parseTimeStamp(theStamp, default, allowNone=False): + """Parses a text representation of a time stamp and converts it into + a float. Note that negative timestamps cause an OSError on Windows. + See https://bugs.python.org/issue29097 + """ + if str(theStamp).lower() == "none" and allowNone: + return None + try: + return datetime.strptime(theStamp, nwConst.FMT_TSTAMP).timestamp() + except Exception: + return default + + +# =============================================================================================== # +# String Functions +# =============================================================================================== # + def splitVersionNumber(vString): """ Splits a version string on the form aa.bb.cc into major, minor and patch, and computes an integer value aabbcc. @@ -308,27 +331,8 @@ def fuzzyTime(secDiff): ).format(int(round(secDiff/31557600))) -def makeFileNameSafe(theText): - """Returns a filename safe version of the text. - """ - cleanName = "" - for c in theText.strip(): - if c.isalpha() or c.isdigit() or c == " ": - cleanName += c - return cleanName - - -def getGuiItem(theName): - """Returns a QtWidget based on its objectName. - """ - for qWidget in qApp.topLevelWidgets(): - if qWidget.objectName() == theName: - return qWidget - return None - - def numberToRoman(numVal, isLower=False): - """Convert an integer to a roman number. + """Convert an integer to a Roman number. """ if not isinstance(numVal, int): return "NAN" @@ -349,3 +353,110 @@ def numberToRoman(numVal, isLower=False): break return romNum.lower() if isLower else romNum + + +# =============================================================================================== # +# Other Functions +# =============================================================================================== # + +def makeFileNameSafe(theText): + """Returns a filename safe version of the text. + """ + cleanName = "" + for c in theText.strip(): + if c.isalpha() or c.isdigit() or c == " ": + cleanName += c + return cleanName + + +def getGuiItem(theName): + """Returns a QtWidget based on its objectName. + """ + for qWidget in qApp.topLevelWidgets(): + if qWidget.objectName() == theName: + return qWidget + return None + + +# =============================================================================================== # +# Classes +# =============================================================================================== # + +class NWConfigParser(ConfigParser): + + CNF_STR = 0 + CNF_INT = 1 + CNF_BOOL = 2 + CNF_S_LST = 3 + CNF_I_LST = 4 + + def __init__(self): + super().__init__() + + def rdStr(self, section, option, default): + """Read string value. + """ + return self._parseLine(section, option, default, self.CNF_STR) + + def rdInt(self, section, option, default): + """Read integer value. + """ + return self._parseLine(section, option, default, self.CNF_INT) + + def rdBool(self, section, option, default): + """Read boolean value. + """ + return self._parseLine(section, option, default, self.CNF_BOOL) + + def rdStrList(self, section, option, default): + """Read string list. + """ + return self._parseLine(section, option, default, self.CNF_S_LST) + + def rdIntList(self, section, option, default): + """Read integer list. + """ + return self._parseLine(section, option, default, self.CNF_I_LST) + + ## + # Internal Functions + ## + + def _unpackList(self, value, default, type): + """Unpack a comma-separated string of items into a list. + """ + inList = value.split(",") + outList = [] + if isinstance(default, list): + outList = default.copy() + for i in range(min(len(inList), len(outList))): + try: + if type == self.CNF_S_LST: + outList[i] = inList[i].strip() + elif type == self.CNF_I_LST: + outList[i] = int(inList[i].strip()) + except Exception: + continue + return outList + + def _parseLine(self, section, option, default, type): + """Parse a line and return the correct datatype. + """ + if self.has_option(section, option): + try: + if type == self.CNF_STR: + return self.get(section, option) + elif type == self.CNF_INT: + return self.getint(section, option) + elif type == self.CNF_BOOL: + return self.getboolean(section, option) + elif type in (self.CNF_I_LST, self.CNF_S_LST): + return self._unpackList(self.get(section, option), default, type) + except ValueError: + logger.error("Could not read '%s':'%s' from config", str(section), str(option)) + logException() + return default + + return default + +# END Class NWConfigParser diff --git a/nw/config.py b/nw/config.py index faabe6ae..b4b8d1b2 100644 --- a/nw/config.py +++ b/nw/config.py @@ -28,7 +28,6 @@ import sys import json import shutil import logging -import configparser from time import time @@ -39,7 +38,7 @@ from PyQt5.QtCore import ( ) from nw.error import logException -from nw.common import splitVersionNumber, formatTimeStamp +from nw.common import splitVersionNumber, formatTimeStamp, NWConfigParser from nw.constants import nwConst, nwFiles, nwUnicode logger = logging.getLogger(__name__) @@ -47,12 +46,6 @@ logger = logging.getLogger(__name__) class Config: - CNF_STR = 0 - CNF_INT = 1 - CNF_BOOL = 2 - CNF_S_LST = 3 - CNF_I_LST = 4 - LANG_NW = 1 LANG_PROJ = 2 @@ -432,11 +425,11 @@ class Config: if self.confPath is None: return False - cnfParse = configparser.ConfigParser() + theConf = NWConfigParser() cnfPath = os.path.join(self.confPath, self.confFile) try: with open(cnfPath, mode="r", encoding="utf-8") as inFile: - cnfParse.read_file(inFile) + theConf.read_file(inFile) except Exception as e: logger.error("Could not load config file") logException() @@ -447,237 +440,95 @@ class Config: # Main cnfSec = "Main" - self.guiTheme = self._parseLine( - cnfParse, cnfSec, "theme", self.CNF_STR, self.guiTheme - ) - self.guiSyntax = self._parseLine( - cnfParse, cnfSec, "syntax", self.CNF_STR, self.guiSyntax - ) - self.guiIcons = self._parseLine( - cnfParse, cnfSec, "icons", self.CNF_STR, self.guiIcons - ) - self.guiDark = self._parseLine( - cnfParse, cnfSec, "guidark", self.CNF_BOOL, self.guiDark - ) - self.guiFont = self._parseLine( - cnfParse, cnfSec, "guifont", self.CNF_STR, self.guiFont - ) - self.guiFontSize = self._parseLine( - cnfParse, cnfSec, "guifontsize", self.CNF_INT, self.guiFontSize - ) - self.lastNotes = self._parseLine( - cnfParse, cnfSec, "lastnotes", self.CNF_STR, self.lastNotes - ) - self.guiLang = self._parseLine( - cnfParse, cnfSec, "guilang", self.CNF_STR, self.guiLang - ) + self.guiTheme = theConf.rdStr(cnfSec, "theme", self.guiTheme) + self.guiSyntax = theConf.rdStr(cnfSec, "syntax", self.guiSyntax) + self.guiIcons = theConf.rdStr(cnfSec, "icons", self.guiIcons) + self.guiDark = theConf.rdBool(cnfSec, "guidark", self.guiDark) + self.guiFont = theConf.rdStr(cnfSec, "guifont", self.guiFont) + self.guiFontSize = theConf.rdInt(cnfSec, "guifontsize", self.guiFontSize) + self.lastNotes = theConf.rdStr(cnfSec, "lastnotes", self.lastNotes) + self.guiLang = theConf.rdStr(cnfSec, "guilang", self.guiLang) # Sizes cnfSec = "Sizes" - self.winGeometry = self._parseLine( - cnfParse, cnfSec, "geometry", self.CNF_I_LST, self.winGeometry - ) - self.prefGeometry = self._parseLine( - cnfParse, cnfSec, "preferences", self.CNF_I_LST, self.prefGeometry - ) - self.treeColWidth = self._parseLine( - cnfParse, cnfSec, "treecols", self.CNF_I_LST, self.treeColWidth - ) - self.novelColWidth = self._parseLine( - cnfParse, cnfSec, "novelcols", self.CNF_I_LST, self.novelColWidth - ) - self.projColWidth = self._parseLine( - cnfParse, cnfSec, "projcols", self.CNF_I_LST, self.projColWidth - ) - self.mainPanePos = self._parseLine( - cnfParse, cnfSec, "mainpane", self.CNF_I_LST, self.mainPanePos - ) - self.docPanePos = self._parseLine( - cnfParse, cnfSec, "docpane", self.CNF_I_LST, self.docPanePos - ) - self.viewPanePos = self._parseLine( - cnfParse, cnfSec, "viewpane", self.CNF_I_LST, self.viewPanePos - ) - self.outlnPanePos = self._parseLine( - cnfParse, cnfSec, "outlinepane", self.CNF_I_LST, self.outlnPanePos - ) - self.isFullScreen = self._parseLine( - cnfParse, cnfSec, "fullscreen", self.CNF_BOOL, self.isFullScreen - ) - self.hideVScroll = self._parseLine( - cnfParse, cnfSec, "hidevscroll", self.CNF_BOOL, self.hideVScroll - ) - self.hideHScroll = self._parseLine( - cnfParse, cnfSec, "hidehscroll", self.CNF_BOOL, self.hideHScroll - ) + self.winGeometry = theConf.rdIntList(cnfSec, "geometry", self.winGeometry) + self.prefGeometry = theConf.rdIntList(cnfSec, "preferences", self.prefGeometry) + self.treeColWidth = theConf.rdIntList(cnfSec, "treecols", self.treeColWidth) + self.novelColWidth = theConf.rdIntList(cnfSec, "novelcols", self.novelColWidth) + self.projColWidth = theConf.rdIntList(cnfSec, "projcols", self.projColWidth) + self.mainPanePos = theConf.rdIntList(cnfSec, "mainpane", self.mainPanePos) + self.docPanePos = theConf.rdIntList(cnfSec, "docpane", self.docPanePos) + self.viewPanePos = theConf.rdIntList(cnfSec, "viewpane", self.viewPanePos) + self.outlnPanePos = theConf.rdIntList(cnfSec, "outlinepane", self.outlnPanePos) + self.isFullScreen = theConf.rdBool(cnfSec, "fullscreen", self.isFullScreen) + self.hideVScroll = theConf.rdBool(cnfSec, "hidevscroll", self.hideVScroll) + self.hideHScroll = theConf.rdBool(cnfSec, "hidehscroll", self.hideHScroll) # Project cnfSec = "Project" - self.autoSaveProj = self._parseLine( - cnfParse, cnfSec, "autosaveproject", self.CNF_INT, self.autoSaveProj - ) - self.autoSaveDoc = self._parseLine( - cnfParse, cnfSec, "autosavedoc", self.CNF_INT, self.autoSaveDoc - ) + self.autoSaveProj = theConf.rdInt(cnfSec, "autosaveproject", self.autoSaveProj) + self.autoSaveDoc = theConf.rdInt(cnfSec, "autosavedoc", self.autoSaveDoc) # Editor cnfSec = "Editor" - self.textFont = self._parseLine( - cnfParse, cnfSec, "textfont", self.CNF_STR, self.textFont - ) - self.textSize = self._parseLine( - cnfParse, cnfSec, "textsize", self.CNF_INT, self.textSize - ) - self.textFixedW = self._parseLine( - cnfParse, cnfSec, "fixedwidth", self.CNF_BOOL, self.textFixedW - ) - self.textWidth = self._parseLine( - cnfParse, cnfSec, "width", self.CNF_INT, self.textWidth - ) - self.textMargin = self._parseLine( - cnfParse, cnfSec, "margin", self.CNF_INT, self.textMargin - ) - self.tabWidth = self._parseLine( - cnfParse, cnfSec, "tabwidth", self.CNF_INT, self.tabWidth - ) - self.focusWidth = self._parseLine( - cnfParse, cnfSec, "focuswidth", self.CNF_INT, self.focusWidth - ) - self.hideFocusFooter = self._parseLine( - cnfParse, cnfSec, "hidefocusfooter", self.CNF_BOOL, self.hideFocusFooter - ) - self.doJustify = self._parseLine( - cnfParse, cnfSec, "justify", self.CNF_BOOL, self.doJustify - ) - self.autoSelect = self._parseLine( - cnfParse, cnfSec, "autoselect", self.CNF_BOOL, self.autoSelect - ) - self.doReplace = self._parseLine( - cnfParse, cnfSec, "autoreplace", self.CNF_BOOL, self.doReplace - ) - self.doReplaceSQuote = self._parseLine( - cnfParse, cnfSec, "repsquotes", self.CNF_BOOL, self.doReplaceSQuote - ) - self.doReplaceDQuote = self._parseLine( - cnfParse, cnfSec, "repdquotes", self.CNF_BOOL, self.doReplaceDQuote - ) - self.doReplaceDash = self._parseLine( - cnfParse, cnfSec, "repdash", self.CNF_BOOL, self.doReplaceDash - ) - self.doReplaceDots = self._parseLine( - cnfParse, cnfSec, "repdots", self.CNF_BOOL, self.doReplaceDots - ) - self.scrollPastEnd = self._parseLine( - cnfParse, cnfSec, "scrollpastend", self.CNF_BOOL, self.scrollPastEnd - ) - self.autoScroll = self._parseLine( - cnfParse, cnfSec, "autoscroll", self.CNF_BOOL, self.autoScroll - ) - self.autoScrollPos = self._parseLine( - cnfParse, cnfSec, "autoscrollpos", self.CNF_INT, self.autoScrollPos - ) - self.fmtSingleQuotes = self._parseLine( - cnfParse, cnfSec, "fmtsinglequote", self.CNF_S_LST, self.fmtSingleQuotes - ) - self.fmtDoubleQuotes = self._parseLine( - cnfParse, cnfSec, "fmtdoublequote", self.CNF_S_LST, self.fmtDoubleQuotes - ) - self.fmtPadBefore = self._parseLine( - cnfParse, cnfSec, "fmtpadbefore", self.CNF_STR, self.fmtPadBefore - ) - self.fmtPadAfter = self._parseLine( - cnfParse, cnfSec, "fmtpadafter", self.CNF_STR, self.fmtPadAfter - ) - self.fmtPadThin = self._parseLine( - cnfParse, cnfSec, "fmtpadthin", self.CNF_BOOL, self.fmtPadThin - ) - self.spellTool = self._parseLine( - cnfParse, cnfSec, "spelltool", self.CNF_STR, self.spellTool - ) - self.spellLanguage = self._parseLine( - cnfParse, cnfSec, "spellcheck", self.CNF_STR, self.spellLanguage - ) - self.showTabsNSpaces = self._parseLine( - cnfParse, cnfSec, "showtabsnspaces", self.CNF_BOOL, self.showTabsNSpaces - ) - self.showLineEndings = self._parseLine( - cnfParse, cnfSec, "showlineendings", self.CNF_BOOL, self.showLineEndings - ) - self.showMultiSpaces = self._parseLine( - cnfParse, cnfSec, "showmultispaces", self.CNF_BOOL, self.showMultiSpaces - ) - self.bigDocLimit = self._parseLine( - cnfParse, cnfSec, "bigdoclimit", self.CNF_INT, self.bigDocLimit - ) - self.showFullPath = self._parseLine( - cnfParse, cnfSec, "showfullpath", self.CNF_BOOL, self.showFullPath - ) - self.highlightQuotes = self._parseLine( - cnfParse, cnfSec, "highlightquotes", self.CNF_BOOL, self.highlightQuotes - ) - self.allowOpenSQuote = self._parseLine( - cnfParse, cnfSec, "allowopensquote", self.CNF_BOOL, self.allowOpenSQuote - ) - self.allowOpenDQuote = self._parseLine( - cnfParse, cnfSec, "allowopendquote", self.CNF_BOOL, self.allowOpenDQuote - ) - self.highlightEmph = self._parseLine( - cnfParse, cnfSec, "highlightemph", self.CNF_BOOL, self.highlightEmph - ) - self.stopWhenIdle = self._parseLine( - cnfParse, cnfSec, "stopwhenidle", self.CNF_BOOL, self.stopWhenIdle - ) - self.userIdleTime = self._parseLine( - cnfParse, cnfSec, "useridletime", self.CNF_INT, self.userIdleTime - ) + self.textFont = theConf.rdStr(cnfSec, "textfont", self.textFont) + self.textSize = theConf.rdInt(cnfSec, "textsize", self.textSize) + self.textFixedW = theConf.rdBool(cnfSec, "fixedwidth", self.textFixedW) + self.textWidth = theConf.rdInt(cnfSec, "width", self.textWidth) + self.textMargin = theConf.rdInt(cnfSec, "margin", self.textMargin) + self.tabWidth = theConf.rdInt(cnfSec, "tabwidth", self.tabWidth) + self.focusWidth = theConf.rdInt(cnfSec, "focuswidth", self.focusWidth) + self.hideFocusFooter = theConf.rdBool(cnfSec, "hidefocusfooter", self.hideFocusFooter) + self.doJustify = theConf.rdBool(cnfSec, "justify", self.doJustify) + self.autoSelect = theConf.rdBool(cnfSec, "autoselect", self.autoSelect) + self.doReplace = theConf.rdBool(cnfSec, "autoreplace", self.doReplace) + self.doReplaceSQuote = theConf.rdBool(cnfSec, "repsquotes", self.doReplaceSQuote) + self.doReplaceDQuote = theConf.rdBool(cnfSec, "repdquotes", self.doReplaceDQuote) + self.doReplaceDash = theConf.rdBool(cnfSec, "repdash", self.doReplaceDash) + self.doReplaceDots = theConf.rdBool(cnfSec, "repdots", self.doReplaceDots) + self.scrollPastEnd = theConf.rdBool(cnfSec, "scrollpastend", self.scrollPastEnd) + self.autoScroll = theConf.rdBool(cnfSec, "autoscroll", self.autoScroll) + self.autoScrollPos = theConf.rdInt(cnfSec, "autoscrollpos", self.autoScrollPos) + self.fmtSingleQuotes = theConf.rdStrList(cnfSec, "fmtsinglequote", self.fmtSingleQuotes) + self.fmtDoubleQuotes = theConf.rdStrList(cnfSec, "fmtdoublequote", self.fmtDoubleQuotes) + self.fmtPadBefore = theConf.rdStr(cnfSec, "fmtpadbefore", self.fmtPadBefore) + self.fmtPadAfter = theConf.rdStr(cnfSec, "fmtpadafter", self.fmtPadAfter) + self.fmtPadThin = theConf.rdBool(cnfSec, "fmtpadthin", self.fmtPadThin) + self.spellTool = theConf.rdStr(cnfSec, "spelltool", self.spellTool) + self.spellLanguage = theConf.rdStr(cnfSec, "spellcheck", self.spellLanguage) + self.showTabsNSpaces = theConf.rdBool(cnfSec, "showtabsnspaces", self.showTabsNSpaces) + self.showLineEndings = theConf.rdBool(cnfSec, "showlineendings", self.showLineEndings) + self.showMultiSpaces = theConf.rdBool(cnfSec, "showmultispaces", self.showMultiSpaces) + self.bigDocLimit = theConf.rdInt(cnfSec, "bigdoclimit", self.bigDocLimit) + self.showFullPath = theConf.rdBool(cnfSec, "showfullpath", self.showFullPath) + self.highlightQuotes = theConf.rdBool(cnfSec, "highlightquotes", self.highlightQuotes) + self.allowOpenSQuote = theConf.rdBool(cnfSec, "allowopensquote", self.allowOpenSQuote) + self.allowOpenDQuote = theConf.rdBool(cnfSec, "allowopendquote", self.allowOpenDQuote) + self.highlightEmph = theConf.rdBool(cnfSec, "highlightemph", self.highlightEmph) + self.stopWhenIdle = theConf.rdBool(cnfSec, "stopwhenidle", self.stopWhenIdle) + self.userIdleTime = theConf.rdInt(cnfSec, "useridletime", self.userIdleTime) # Backup cnfSec = "Backup" - self.backupPath = self._parseLine( - cnfParse, cnfSec, "backuppath", self.CNF_STR, self.backupPath - ) - self.backupOnClose = self._parseLine( - cnfParse, cnfSec, "backuponclose", self.CNF_BOOL, self.backupOnClose - ) - self.askBeforeBackup = self._parseLine( - cnfParse, cnfSec, "askbeforebackup", self.CNF_BOOL, self.askBeforeBackup - ) + self.backupPath = theConf.rdStr(cnfSec, "backuppath", self.backupPath) + self.backupOnClose = theConf.rdBool(cnfSec, "backuponclose", self.backupOnClose) + self.askBeforeBackup = theConf.rdBool(cnfSec, "askbeforebackup", self.askBeforeBackup) # State cnfSec = "State" - self.showRefPanel = self._parseLine( - cnfParse, cnfSec, "showrefpanel", self.CNF_BOOL, self.showRefPanel - ) - self.viewComments = self._parseLine( - cnfParse, cnfSec, "viewcomments", self.CNF_BOOL, self.viewComments - ) - self.viewSynopsis = self._parseLine( - cnfParse, cnfSec, "viewsynopsis", self.CNF_BOOL, self.viewSynopsis - ) - self.searchCase = self._parseLine( - cnfParse, cnfSec, "searchcase", self.CNF_BOOL, self.searchCase - ) - self.searchWord = self._parseLine( - cnfParse, cnfSec, "searchword", self.CNF_BOOL, self.searchWord - ) - self.searchRegEx = self._parseLine( - cnfParse, cnfSec, "searchregex", self.CNF_BOOL, self.searchRegEx - ) - self.searchLoop = self._parseLine( - cnfParse, cnfSec, "searchloop", self.CNF_BOOL, self.searchLoop - ) - self.searchNextFile = self._parseLine( - cnfParse, cnfSec, "searchnextfile", self.CNF_BOOL, self.searchNextFile - ) - self.searchMatchCap = self._parseLine( - cnfParse, cnfSec, "searchmatchcap", self.CNF_BOOL, self.searchMatchCap - ) + self.showRefPanel = theConf.rdBool(cnfSec, "showrefpanel", self.showRefPanel) + self.viewComments = theConf.rdBool(cnfSec, "viewcomments", self.viewComments) + self.viewSynopsis = theConf.rdBool(cnfSec, "viewsynopsis", self.viewSynopsis) + self.searchCase = theConf.rdBool(cnfSec, "searchcase", self.searchCase) + self.searchWord = theConf.rdBool(cnfSec, "searchword", self.searchWord) + self.searchRegEx = theConf.rdBool(cnfSec, "searchregex", self.searchRegEx) + self.searchLoop = theConf.rdBool(cnfSec, "searchloop", self.searchLoop) + self.searchNextFile = theConf.rdBool(cnfSec, "searchnextfile", self.searchNextFile) + self.searchMatchCap = theConf.rdBool(cnfSec, "searchmatchcap", self.searchMatchCap) # Path cnfSec = "Path" - self.lastPath = self._parseLine( - cnfParse, cnfSec, "lastpath", self.CNF_STR, self.lastPath - ) + self.lastPath = theConf.rdStr(cnfSec, "lastpath", self.lastPath) # Check Certain Values for None self.spellLanguage = self._checkNone(self.spellLanguage) @@ -700,115 +551,106 @@ class Config: if self.confPath is None: return False - cnfParse = configparser.ConfigParser() + theConf = NWConfigParser() - # Set options + theConf["Main"] = { + "timestamp": formatTimeStamp(time()), + "theme": str(self.guiTheme), + "syntax": str(self.guiSyntax), + "icons": str(self.guiIcons), + "guidark": str(self.guiDark), + "guifont": str(self.guiFont), + "guifontsize": str(self.guiFontSize), + "lastnotes": str(self.lastNotes), + "guilang": str(self.guiLang), + } - # Main - cnfSec = "Main" - cnfParse.add_section(cnfSec) - cnfParse.set(cnfSec, "timestamp", formatTimeStamp(time())) - cnfParse.set(cnfSec, "theme", str(self.guiTheme)) - cnfParse.set(cnfSec, "syntax", str(self.guiSyntax)) - cnfParse.set(cnfSec, "icons", str(self.guiIcons)) - cnfParse.set(cnfSec, "guidark", str(self.guiDark)) - cnfParse.set(cnfSec, "guifont", str(self.guiFont)) - cnfParse.set(cnfSec, "guifontsize", str(self.guiFontSize)) - cnfParse.set(cnfSec, "lastnotes", str(self.lastNotes)) - cnfParse.set(cnfSec, "guilang", str(self.guiLang)) + theConf["Sizes"] = { + "geometry": self._packList(self.winGeometry), + "preferences": self._packList(self.prefGeometry), + "treecols": self._packList(self.treeColWidth), + "novelcols": self._packList(self.novelColWidth), + "projcols": self._packList(self.projColWidth), + "mainpane": self._packList(self.mainPanePos), + "docpane": self._packList(self.docPanePos), + "viewpane": self._packList(self.viewPanePos), + "outlinepane": self._packList(self.outlnPanePos), + "fullscreen": str(self.isFullScreen), + "hidevscroll": str(self.hideVScroll), + "hidehscroll": str(self.hideHScroll), + } - # Sizes - cnfSec = "Sizes" - cnfParse.add_section(cnfSec) - cnfParse.set(cnfSec, "geometry", self._packList(self.winGeometry)) - cnfParse.set(cnfSec, "preferences", self._packList(self.prefGeometry)) - cnfParse.set(cnfSec, "treecols", self._packList(self.treeColWidth)) - cnfParse.set(cnfSec, "novelcols", self._packList(self.novelColWidth)) - cnfParse.set(cnfSec, "projcols", self._packList(self.projColWidth)) - cnfParse.set(cnfSec, "mainpane", self._packList(self.mainPanePos)) - cnfParse.set(cnfSec, "docpane", self._packList(self.docPanePos)) - cnfParse.set(cnfSec, "viewpane", self._packList(self.viewPanePos)) - cnfParse.set(cnfSec, "outlinepane", self._packList(self.outlnPanePos)) - cnfParse.set(cnfSec, "fullscreen", str(self.isFullScreen)) - cnfParse.set(cnfSec, "hidevscroll", str(self.hideVScroll)) - cnfParse.set(cnfSec, "hidehscroll", str(self.hideHScroll)) + theConf["Project"] = { + "autosaveproject": str(self.autoSaveProj), + "autosavedoc": str(self.autoSaveDoc), + } - # Project - cnfSec = "Project" - cnfParse.add_section(cnfSec) - cnfParse.set(cnfSec, "autosaveproject", str(self.autoSaveProj)) - cnfParse.set(cnfSec, "autosavedoc", str(self.autoSaveDoc)) + theConf["Editor"] = { + "textfont": str(self.textFont), + "textsize": str(self.textSize), + "fixedwidth": str(self.textFixedW), + "width": str(self.textWidth), + "margin": str(self.textMargin), + "tabwidth": str(self.tabWidth), + "focuswidth": str(self.focusWidth), + "hidefocusfooter": str(self.hideFocusFooter), + "justify": str(self.doJustify), + "autoselect": str(self.autoSelect), + "autoreplace": str(self.doReplace), + "repsquotes": str(self.doReplaceSQuote), + "repdquotes": str(self.doReplaceDQuote), + "repdash": str(self.doReplaceDash), + "repdots": str(self.doReplaceDots), + "scrollpastend": str(self.scrollPastEnd), + "autoscroll": str(self.autoScroll), + "autoscrollpos": str(self.autoScrollPos), + "fmtsinglequote": self._packList(self.fmtSingleQuotes), + "fmtdoublequote": self._packList(self.fmtDoubleQuotes), + "fmtpadbefore": str(self.fmtPadBefore), + "fmtpadafter": str(self.fmtPadAfter), + "fmtpadthin": str(self.fmtPadThin), + "spelltool": str(self.spellTool), + "spellcheck": str(self.spellLanguage), + "showtabsnspaces": str(self.showTabsNSpaces), + "showlineendings": str(self.showLineEndings), + "showmultispaces": str(self.showMultiSpaces), + "bigdoclimit": str(self.bigDocLimit), + "showfullpath": str(self.showFullPath), + "highlightquotes": str(self.highlightQuotes), + "allowopensquote": str(self.allowOpenSQuote), + "allowopendquote": str(self.allowOpenDQuote), + "highlightemph": str(self.highlightEmph), + "stopwhenidle": str(self.stopWhenIdle), + "useridletime": str(self.userIdleTime), + } - # Editor - cnfSec = "Editor" - cnfParse.add_section(cnfSec) - cnfParse.set(cnfSec, "textfont", str(self.textFont)) - cnfParse.set(cnfSec, "textsize", str(self.textSize)) - cnfParse.set(cnfSec, "fixedwidth", str(self.textFixedW)) - cnfParse.set(cnfSec, "width", str(self.textWidth)) - cnfParse.set(cnfSec, "margin", str(self.textMargin)) - cnfParse.set(cnfSec, "tabwidth", str(self.tabWidth)) - cnfParse.set(cnfSec, "focuswidth", str(self.focusWidth)) - cnfParse.set(cnfSec, "hidefocusfooter", str(self.hideFocusFooter)) - cnfParse.set(cnfSec, "justify", str(self.doJustify)) - cnfParse.set(cnfSec, "autoselect", str(self.autoSelect)) - cnfParse.set(cnfSec, "autoreplace", str(self.doReplace)) - cnfParse.set(cnfSec, "repsquotes", str(self.doReplaceSQuote)) - cnfParse.set(cnfSec, "repdquotes", str(self.doReplaceDQuote)) - cnfParse.set(cnfSec, "repdash", str(self.doReplaceDash)) - cnfParse.set(cnfSec, "repdots", str(self.doReplaceDots)) - cnfParse.set(cnfSec, "scrollpastend", str(self.scrollPastEnd)) - cnfParse.set(cnfSec, "autoscroll", str(self.autoScroll)) - cnfParse.set(cnfSec, "autoscrollpos", str(self.autoScrollPos)) - cnfParse.set(cnfSec, "fmtsinglequote", self._packList(self.fmtSingleQuotes)) - cnfParse.set(cnfSec, "fmtdoublequote", self._packList(self.fmtDoubleQuotes)) - cnfParse.set(cnfSec, "fmtpadbefore", str(self.fmtPadBefore)) - cnfParse.set(cnfSec, "fmtpadafter", str(self.fmtPadAfter)) - cnfParse.set(cnfSec, "fmtpadthin", str(self.fmtPadThin)) - cnfParse.set(cnfSec, "spelltool", str(self.spellTool)) - cnfParse.set(cnfSec, "spellcheck", str(self.spellLanguage)) - cnfParse.set(cnfSec, "showtabsnspaces", str(self.showTabsNSpaces)) - cnfParse.set(cnfSec, "showlineendings", str(self.showLineEndings)) - cnfParse.set(cnfSec, "showmultispaces", str(self.showMultiSpaces)) - cnfParse.set(cnfSec, "bigdoclimit", str(self.bigDocLimit)) - cnfParse.set(cnfSec, "showfullpath", str(self.showFullPath)) - cnfParse.set(cnfSec, "highlightquotes", str(self.highlightQuotes)) - cnfParse.set(cnfSec, "allowopensquote", str(self.allowOpenSQuote)) - cnfParse.set(cnfSec, "allowopendquote", str(self.allowOpenDQuote)) - cnfParse.set(cnfSec, "highlightemph", str(self.highlightEmph)) - cnfParse.set(cnfSec, "stopwhenidle", str(self.stopWhenIdle)) - cnfParse.set(cnfSec, "useridletime", str(self.userIdleTime)) + theConf["Backup"] = { + "backuppath": str(self.backupPath), + "backuponclose": str(self.backupOnClose), + "askbeforebackup": str(self.askBeforeBackup), + } - # Backup - cnfSec = "Backup" - cnfParse.add_section(cnfSec) - cnfParse.set(cnfSec, "backuppath", str(self.backupPath)) - cnfParse.set(cnfSec, "backuponclose", str(self.backupOnClose)) - cnfParse.set(cnfSec, "askbeforebackup", str(self.askBeforeBackup)) + theConf["State"] = { + "showrefpanel": str(self.showRefPanel), + "viewcomments": str(self.viewComments), + "viewsynopsis": str(self.viewSynopsis), + "searchcase": str(self.searchCase), + "searchword": str(self.searchWord), + "searchregex": str(self.searchRegEx), + "searchloop": str(self.searchLoop), + "searchnextfile": str(self.searchNextFile), + "searchmatchcap": str(self.searchMatchCap), + } - # State - cnfSec = "State" - cnfParse.add_section(cnfSec) - cnfParse.set(cnfSec, "showrefpanel", str(self.showRefPanel)) - cnfParse.set(cnfSec, "viewcomments", str(self.viewComments)) - cnfParse.set(cnfSec, "viewsynopsis", str(self.viewSynopsis)) - cnfParse.set(cnfSec, "searchcase", str(self.searchCase)) - cnfParse.set(cnfSec, "searchword", str(self.searchWord)) - cnfParse.set(cnfSec, "searchregex", str(self.searchRegEx)) - cnfParse.set(cnfSec, "searchloop", str(self.searchLoop)) - cnfParse.set(cnfSec, "searchnextfile", str(self.searchNextFile)) - cnfParse.set(cnfSec, "searchmatchcap", str(self.searchMatchCap)) - - # Path - cnfSec = "Path" - cnfParse.add_section(cnfSec) - cnfParse.set(cnfSec, "lastpath", str(self.lastPath)) + theConf["Path"] = { + "lastpath": str(self.lastPath), + } # Write config file cnfPath = os.path.join(self.confPath, self.confFile) try: with open(cnfPath, mode="w", encoding="utf-8") as outFile: - cnfParse.write(outFile) + theConf.write(outFile) self.confChanged = False except Exception as e: logger.error("Could not save config file") @@ -884,7 +726,7 @@ class Config: return True def updateRecentCache(self, projPath, projTitle, wordCount, saveTime): - """Add or update recent cache information o9n a given project. + """Add or update recent cache information on a given project. """ self.recentProj[os.path.abspath(projPath)] = { "title": projTitle, @@ -992,12 +834,6 @@ class Config: self.confChanged = True return self.showRefPanel - def getErrData(self): - errMessage = "
".join(self.errData) - self.hasError = False - self.errData = [] - return errMessage - def setViewComments(self, viewState): self.viewComments = viewState self.confChanged = True @@ -1051,6 +887,12 @@ class Config: def getFocusWidth(self): return self.pxInt(self.focusWidth) + def getErrData(self): + errMessage = "
".join(self.errData) + self.hasError = False + self.errData = [] + return errMessage + ## # Internal Functions ## @@ -1060,52 +902,8 @@ class Config: """ return ", ".join([str(inVal) for inVal in inData]) - def _unpackList(self, inStr, listDefault, cnfType): - """Unpack a comma-separated string of items into a list. - """ - inData = inStr.split(",") - outData = listDefault.copy() - for i in range(min(len(inData), len(listDefault))): - try: - if cnfType == self.CNF_S_LST: - outData[i] = inData[i].strip() - elif cnfType == self.CNF_I_LST: - outData[i] = int(inData[i].strip()) - else: - continue - except Exception: - continue - return outData - - def _parseLine(self, cnfParse, cnfSec, cnfName, cnfType, cnfDefault): - """Parse a line and return the correct datatype. - """ - if cnfParse.has_section(cnfSec): - if cnfParse.has_option(cnfSec, cnfName): - try: - if cnfType == self.CNF_STR: - return cnfParse.get(cnfSec, cnfName) - elif cnfType == self.CNF_INT: - return cnfParse.getint(cnfSec, cnfName) - elif cnfType == self.CNF_BOOL: - return cnfParse.getboolean(cnfSec, cnfName) - elif cnfType == self.CNF_I_LST: - return self._unpackList( - cnfParse.get(cnfSec, cnfName), cnfDefault, self.CNF_I_LST - ) - elif cnfType == self.CNF_S_LST: - return self._unpackList( - cnfParse.get(cnfSec, cnfName), cnfDefault, self.CNF_S_LST - ) - except ValueError: - logger.error("Failed to load value from config file.") - logException() - return cnfDefault - - return cnfDefault - def _checkNone(self, checkVal): - """Return a NoneType if the value correspomds to None, otherwise + """Return a NoneType if the value corresponds to None, otherwise return the value unchanged. """ if checkVal is None: diff --git a/nw/core/index.py b/nw/core/index.py index 01bdb69d..d3cfc18e 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -257,10 +257,10 @@ class NWIndex(): # If the file is archived or trashed, we don't index the file itself if self.theProject.projTree.isTrashRoot(theItem.itemParent): - logger.info("Not indexing trash item '%s'", tHandle) + logger.debug("Not indexing trash item '%s'", tHandle) return False if theRoot.itemClass == nwItemClass.ARCHIVE: - logger.info("Not indexing archived item '%s'", tHandle) + logger.debug("Not indexing archived item '%s'", tHandle) return False itemClass = theItem.itemClass diff --git a/nw/core/item.py b/nw/core/item.py index 9da762c0..a61e8a0b 100644 --- a/nw/core/item.py +++ b/nw/core/item.py @@ -251,7 +251,7 @@ class NWItem(): if isinstance(expState, str): self.isExpanded = (expState == str(True)) else: - self.isExpanded = (expState == True) # noqa: E712 + self.isExpanded = (expState is True) return def setExported(self, expState): @@ -260,7 +260,7 @@ class NWItem(): if isinstance(expState, str): self.isExported = (expState == str(True)) else: - self.isExported = (expState == True) # noqa: E712 + self.isExported = (expState is True) return ## diff --git a/nw/core/project.py b/nw/core/project.py index 752178af..ccdfcc45 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -369,7 +369,7 @@ class NWProject(): self.clearProject() self.projPath = os.path.abspath(os.path.dirname(fileName)) - logger.debug("Opening project: %s", self.projPath) + logger.info("Opening project: %s", self.projPath) # Standard Folders and Files # ========================== @@ -611,7 +611,7 @@ class NWProject(): if not self.ensureFolderStructure(): return False - logger.debug("Saving project: %s", self.projPath) + logger.info("Saving project: %s", self.projPath) if autoSave: self.autoCount += 1 @@ -712,6 +712,7 @@ class NWProject(): def closeProject(self, idleTime=0): """Close the current project and clear all meta data. """ + logger.info("Closing project: %s", self.projPath) self.optState.saveSettings() self.projTree.writeToCFile() self._appendSessionStats(idleTime) diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 1932ace8..d493aae9 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -27,7 +27,6 @@ along with this program. If not, see . import nw import os import logging -import configparser from math import ceil from functools import partial @@ -39,6 +38,7 @@ from PyQt5.QtGui import ( ) from nw.enum import nwAlert +from nw.common import NWConfigParser logger = logging.getLogger(__name__) @@ -280,7 +280,7 @@ class GuiTheme: return False # Config File - confParser = configparser.ConfigParser() + confParser = NWConfigParser() try: with open(self.confFile, mode="r", encoding="utf-8") as inFile: confParser.read_file(inFile) @@ -292,13 +292,13 @@ class GuiTheme: # Main cnfSec = "Main" if confParser.has_section(cnfSec): - self.themeName = self._parseLine(confParser, cnfSec, "name", "") - self.themeDescription = self._parseLine(confParser, cnfSec, "description", "N/A") - self.themeAuthor = self._parseLine(confParser, cnfSec, "author", "N/A") - self.themeCredit = self._parseLine(confParser, cnfSec, "credit", "N/A") - self.themeUrl = self._parseLine(confParser, cnfSec, "url", "") - self.themeLicense = self._parseLine(confParser, cnfSec, "license", "N/A") - self.themeLicenseUrl = self._parseLine(confParser, cnfSec, "licenseurl", "") + self.themeName = confParser.rdStr(cnfSec, "name", "") + self.themeDescription = confParser.rdStr(cnfSec, "description", "N/A") + self.themeAuthor = confParser.rdStr(cnfSec, "author", "N/A") + self.themeCredit = confParser.rdStr(cnfSec, "credit", "N/A") + self.themeUrl = confParser.rdStr(cnfSec, "url", "") + self.themeLicense = confParser.rdStr(cnfSec, "license", "N/A") + self.themeLicenseUrl = confParser.rdStr(cnfSec, "licenseurl", "") # Palette cnfSec = "Palette" @@ -338,7 +338,7 @@ class GuiTheme: """ logger.debug("Loading syntax theme files") - confParser = configparser.ConfigParser() + confParser = NWConfigParser() try: with open(self.syntaxFile, mode="r", encoding="utf-8") as inFile: confParser.read_file(inFile) @@ -350,13 +350,13 @@ class GuiTheme: # Main cnfSec = "Main" if confParser.has_section(cnfSec): - self.syntaxName = self._parseLine(confParser, cnfSec, "name", "") - self.syntaxDescription = self._parseLine(confParser, cnfSec, "description", "") - self.syntaxAuthor = self._parseLine(confParser, cnfSec, "author", "") - self.syntaxCredit = self._parseLine(confParser, cnfSec, "credit", "") - self.syntaxUrl = self._parseLine(confParser, cnfSec, "url", "") - self.syntaxLicense = self._parseLine(confParser, cnfSec, "license", "") - self.syntaxLicenseUrl = self._parseLine(confParser, cnfSec, "licenseurl", "") + self.syntaxName = confParser.rdStr(cnfSec, "name", "") + self.syntaxDescription = confParser.rdStr(cnfSec, "description", "") + self.syntaxAuthor = confParser.rdStr(cnfSec, "author", "") + self.syntaxCredit = confParser.rdStr(cnfSec, "credit", "") + self.syntaxUrl = confParser.rdStr(cnfSec, "url", "") + self.syntaxLicense = confParser.rdStr(cnfSec, "license", "") + self.syntaxLicenseUrl = confParser.rdStr(cnfSec, "licenseurl", "") # Syntax cnfSec = "Syntax" @@ -388,7 +388,7 @@ class GuiTheme: if self.themeList: return self.themeList - confParser = configparser.ConfigParser() + confParser = NWConfigParser() for themeDir in os.listdir(os.path.join(self.mainConf.themeRoot, self.guiPath)): themeConf = os.path.join( self.mainConf.themeRoot, self.guiPath, themeDir, self.confName @@ -420,7 +420,7 @@ class GuiTheme: if self.syntaxList: return self.syntaxList - confParser = configparser.ConfigParser() + confParser = NWConfigParser() syntaxDir = os.path.join(self.mainConf.themeRoot, self.syntaxPath) for syntaxFile in os.listdir(syntaxDir): syntaxPath = os.path.join(syntaxDir, syntaxFile) @@ -486,15 +486,6 @@ class GuiTheme: self.guiPalette.setColor(paletteVal, QColor(*readCol)) return - def _parseLine(self, confParser, cnfSec, cnfName, cnfDefault): - """Simple wrapper for the config parser to check that the entry - exists before attempting to load it. - """ - if confParser.has_section(cnfSec): - if confParser.has_option(cnfSec, cnfName): - return confParser.get(cnfSec, cnfName) - return cnfDefault - # End Class GuiTheme @@ -644,7 +635,7 @@ class GuiIcons: return False # Config File - confParser = configparser.ConfigParser() + confParser = NWConfigParser() try: with open(self.confFile, mode="r", encoding="utf-8") as inFile: confParser.read_file(inFile) @@ -656,13 +647,13 @@ class GuiIcons: # Main cnfSec = "Main" if confParser.has_section(cnfSec): - self.themeName = self._parseLine(confParser, cnfSec, "name", "") - self.themeDescription = self._parseLine(confParser, cnfSec, "description", "") - self.themeAuthor = self._parseLine(confParser, cnfSec, "author", "N/A") - self.themeCredit = self._parseLine(confParser, cnfSec, "credit", "N/A") - self.themeUrl = self._parseLine(confParser, cnfSec, "url", "") - self.themeLicense = self._parseLine(confParser, cnfSec, "license", "N/A") - self.themeLicenseUrl = self._parseLine(confParser, cnfSec, "licenseurl", "") + self.themeName = confParser.rdStr(cnfSec, "name", "") + self.themeDescription = confParser.rdStr(cnfSec, "description", "") + self.themeAuthor = confParser.rdStr(cnfSec, "author", "N/A") + self.themeCredit = confParser.rdStr(cnfSec, "credit", "N/A") + self.themeUrl = confParser.rdStr(cnfSec, "url", "") + self.themeLicense = confParser.rdStr(cnfSec, "license", "N/A") + self.themeLicenseUrl = confParser.rdStr(cnfSec, "licenseurl", "") # Palette cnfSec = "Map" @@ -736,7 +727,7 @@ class GuiIcons: if self.themeList: return self.themeList - confParser = configparser.ConfigParser() + confParser = NWConfigParser() for themeDir in os.listdir(self.mainConf.iconPath): themePath = os.path.join(self.mainConf.iconPath, themeDir) if not os.path.isdir(themePath) or themeDir == self.fbackName: @@ -818,13 +809,4 @@ class GuiIcons: return QIcon() - def _parseLine(self, confParser, cnfSec, cnfName, cnfDefault): - """Simple wrapper for the config parser to check that the entry - exists before attempting to load it. - """ - if confParser.has_section(cnfSec): - if confParser.has_option(cnfSec, cnfName): - return confParser.get(cnfSec, cnfName) - return cnfDefault - # END Class GuiIcons diff --git a/nw/guimain.py b/nw/guimain.py index c485988f..3ca7ef7d 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -71,12 +71,9 @@ class GuiMain(QMainWindow): logger.info("OS: %s", self.mainConf.osType) logger.info("Kernel: %s", self.mainConf.kernelVer) logger.info("Host: %s", self.mainConf.hostName) - logger.info("Qt5 Version: %s (%d)", - self.mainConf.verQtString, self.mainConf.verQtValue) - logger.info("PyQt5 Version: %s (%d)", - self.mainConf.verPyQtString, self.mainConf.verPyQtValue) - logger.info("Python Version: %s (0x%x)", - self.mainConf.verPyString, self.mainConf.verPyHexVal) + logger.info("Qt5: %s (%d)", self.mainConf.verQtString, self.mainConf.verQtValue) + logger.info("PyQt5: %s (%d)", self.mainConf.verPyQtString, self.mainConf.verPyQtValue) + logger.info("Python: %s (0x%x)", self.mainConf.verPyString, self.mainConf.verPyHexVal) logger.info("GUI Language: %s", self.mainConf.guiLang) # Core Classes @@ -297,7 +294,7 @@ class GuiMain(QMainWindow): logger.debug("Opening project from additional command line option") self.openProject(self.mainConf.cmdOpen) - logger.debug("novelWriter is ready ...") + logger.info("novelWriter is ready ...") self.setStatus(self.tr("novelWriter is ready ...")) return @@ -872,7 +869,7 @@ class GuiMain(QMainWindow): logger.error("No project open") return False - logger.debug("Rebuilding index ...") + logger.info("Rebuilding index ...") qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) tStart = time() diff --git a/tests/conftest.py b/tests/conftest.py index 93ed72a1..a1e97928 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -91,10 +91,7 @@ def fncDir(tmpDir): shutil.rmtree(fncDir) if not os.path.isdir(fncDir): os.mkdir(fncDir) - yield fncDir - if os.path.isdir(fncDir): - shutil.rmtree(fncDir) - return + return fncDir @pytest.fixture(scope="function") diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index e5fb98ce..f85aaf06 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -19,14 +19,19 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +import os import time import pytest +from datetime import datetime + +from tools import writeFile + from nw.common import ( - checkString, checkBool, checkInt, formatInt, transferCase, - fuzzyTime, checkHandle, formatTimeStamp, formatTime, hexToInt, + checkString, checkBool, checkInt, formatInt, transferCase, fuzzyTime, + checkHandle, formatTimeStamp, parseTimeStamp, formatTime, hexToInt, makeFileNameSafe, isHandle, isTitleTag, isItemClass, isItemType, - isItemLayout, numberToRoman + isItemLayout, numberToRoman, NWConfigParser ) @@ -65,11 +70,11 @@ def testBaseCommon_CheckBool(): """ assert checkBool(None, 3, True) is None assert checkBool("None", 3, True) is None - assert checkBool("True", False, False) - assert not checkBool("False", True, False) + assert checkBool("True", False, False) is True + assert checkBool("False", True, False) is False assert checkBool("Boo", None, False) is None - assert not checkBool(0, None, False) - assert checkBool(1, None, False) + assert checkBool(0, None, False) is False + assert checkBool(1, None, False) is True assert checkBool(2, None, False) is None assert checkBool(0.0, None, False) is None assert checkBool(1.0, None, False) is None @@ -96,13 +101,12 @@ def testBaseCommon_CheckHandle(): def testBaseCommon_IsHandle(): """Test the isHandle function. """ - assert isHandle("47666c91c7ccf") - - assert not isHandle("47666C91C7CCF") - assert not isHandle("h7666c91c7ccf") - assert not isHandle("None") - assert not isHandle(None) - assert not isHandle("STUFF") + assert isHandle("47666c91c7ccf") is True + assert isHandle("47666C91C7CCF") is False + assert isHandle("h7666c91c7ccf") is False + assert isHandle("None") is False + assert isHandle(None) is False + assert isHandle("STUFF") is False # END Test testBaseCommon_IsHandle @@ -111,16 +115,16 @@ def testBaseCommon_IsHandle(): def testBaseCommon_IsTitleTag(): """Test the isItemClass function. """ - assert isTitleTag("T123456") + assert isTitleTag("T123456") is True - assert not isTitleTag("t123456") - assert not isTitleTag("S123456") - assert not isTitleTag("T12345A") - assert not isTitleTag("T1234567") + assert isTitleTag("t123456") is False + assert isTitleTag("S123456") is False + assert isTitleTag("T12345A") is False + assert isTitleTag("T1234567") is False - assert not isTitleTag("None") - assert not isTitleTag(None) - assert not isTitleTag("STUFF") + assert isTitleTag("None") is False + assert isTitleTag(None) is False + assert isTitleTag("STUFF") is False # END Test testBaseCommon_IsTitleTag @@ -129,21 +133,21 @@ def testBaseCommon_IsTitleTag(): def testBaseCommon_IsItemClass(): """Test the isItemClass function. """ - assert isItemClass("NO_CLASS") - assert isItemClass("NOVEL") - assert isItemClass("PLOT") - assert isItemClass("CHARACTER") - assert isItemClass("WORLD") - assert isItemClass("TIMELINE") - assert isItemClass("OBJECT") - assert isItemClass("ENTITY") - assert isItemClass("CUSTOM") - assert isItemClass("ARCHIVE") - assert isItemClass("TRASH") + assert isItemClass("NO_CLASS") is True + assert isItemClass("NOVEL") is True + assert isItemClass("PLOT") is True + assert isItemClass("CHARACTER") is True + assert isItemClass("WORLD") is True + assert isItemClass("TIMELINE") is True + assert isItemClass("OBJECT") is True + assert isItemClass("ENTITY") is True + assert isItemClass("CUSTOM") is True + assert isItemClass("ARCHIVE") is True + assert isItemClass("TRASH") is True - assert not isItemClass("None") - assert not isItemClass(None) - assert not isItemClass("STUFF") + assert isItemClass("None") is False + assert isItemClass(None) is False + assert isItemClass("STUFF") is False # END Test testBaseCommon_IsItemClass @@ -152,15 +156,15 @@ def testBaseCommon_IsItemClass(): def testBaseCommon_IsItemType(): """Test the isItemType function. """ - assert isItemType("NO_TYPE") - assert isItemType("ROOT") - assert isItemType("FOLDER") - assert isItemType("FILE") - assert isItemType("TRASH") + assert isItemType("NO_TYPE") is True + assert isItemType("ROOT") is True + assert isItemType("FOLDER") is True + assert isItemType("FILE") is True + assert isItemType("TRASH") is True - assert not isItemType("None") - assert not isItemType(None) - assert not isItemType("STUFF") + assert isItemType("None") is False + assert isItemType(None) is False + assert isItemType("STUFF") is False # END Test testBaseCommon_IsItemType @@ -169,19 +173,19 @@ def testBaseCommon_IsItemType(): def testBaseCommon_IsItemLayout(): """Test the isItemLayout function. """ - assert isItemLayout("NO_LAYOUT") - assert isItemLayout("TITLE") - assert isItemLayout("BOOK") - assert isItemLayout("PAGE") - assert isItemLayout("PARTITION") - assert isItemLayout("UNNUMBERED") - assert isItemLayout("CHAPTER") - assert isItemLayout("SCENE") - assert isItemLayout("NOTE") + assert isItemLayout("NO_LAYOUT") is True + assert isItemLayout("TITLE") is True + assert isItemLayout("BOOK") is True + assert isItemLayout("PAGE") is True + assert isItemLayout("PARTITION") is True + assert isItemLayout("UNNUMBERED") is True + assert isItemLayout("CHAPTER") is True + assert isItemLayout("SCENE") is True + assert isItemLayout("NOTE") is True - assert not isItemLayout("None") - assert not isItemLayout(None) - assert not isItemLayout("STUFF") + assert isItemLayout("None") is False + assert isItemLayout(None) is False + assert isItemLayout("STUFF") is False # END Test testBaseCommon_IsItemLayout @@ -234,11 +238,29 @@ def testBaseCommon_FormatTime(): # END Test testBaseCommon_FormatTime +@pytest.mark.base +def testBaseCommon_ParseTimeStamp(): + """Test the parseTimeStamp function. + """ + localEpoch = datetime(2000, 1, 1).timestamp() + assert parseTimeStamp(None, 0.0, allowNone=True) is None + assert parseTimeStamp("None", 0.0, allowNone=True) is None + assert parseTimeStamp("None", 0.0) == 0.0 + assert parseTimeStamp("2000-01-01 00:00:00", 123.0) == localEpoch + assert parseTimeStamp("2000-13-01 00:00:00", 123.0) == 123.0 + assert parseTimeStamp("2000-01-32 00:00:00", 123.0) == 123.0 + +# END Test testBaseCommon_ParseTimeStamp + + @pytest.mark.base def testBaseCommon_FormatInt(): """Test the formatInt function. """ - assert formatInt(1000) == "1000" + # Normal Cases + assert formatInt(1) == "1" + assert formatInt(12) == "12" + assert formatInt(123) == "123" assert formatInt(1234) == "1.23\u2009k" assert formatInt(12345) == "12.3\u2009k" assert formatInt(123456) == "123\u2009k" @@ -247,6 +269,11 @@ def testBaseCommon_FormatInt(): assert formatInt(123456789) == "123\u2009M" assert formatInt(1234567890) == "1.23\u2009G" + # Exceptions + assert formatInt(12.3) == "ERR" + assert formatInt(None) == "ERR" + assert formatInt("42") == "ERR" + # END Test testBaseCommon_FormatInt @@ -339,3 +366,87 @@ def testBaseCommon_RomanNumbers(): assert numberToRoman(999, True) == "cmxcix" # END Test testBaseCommon_RomanNumbers + + +@pytest.mark.base +def testBaseCommon_NWConfigParser(fncDir): + """Test the NWConfigParser subclass. + """ + tstConf = os.path.join(fncDir, "test.cfg") + writeFile(tstConf, ( + "[main]\n" + "stropt = value\n" + "intopt1 = 42\n" + "intopt2 = 42.43\n" + "boolopt1 = true\n" + "boolopt2 = TRUE\n" + "boolopt3 = 1\n" + "boolopt4 = 0\n" + "list1 = a, b, c\n" + "list2 = 17, 18, 19\n" + )) + + cfgParser = NWConfigParser() + cfgParser.read(tstConf) + + # Readers + # ======= + + # Read String + assert cfgParser.rdStr("main", "stropt", "stuff") == "value" + assert cfgParser.rdStr("main", "boolopt1", "stuff") == "true" + assert cfgParser.rdStr("main", "intopt1", "stuff") == "42" + + assert cfgParser.rdStr("nope", "stropt", "stuff") == "stuff" + assert cfgParser.rdStr("main", "blabla", "stuff") == "stuff" + + # Read Boolean + assert cfgParser.rdBool("main", "boolopt1", None) is True + assert cfgParser.rdBool("main", "boolopt2", None) is True + assert cfgParser.rdBool("main", "boolopt3", None) is True + assert cfgParser.rdBool("main", "boolopt4", None) is False + assert cfgParser.rdBool("main", "intopt1", None) is None + + assert cfgParser.rdBool("nope", "boolopt1", None) is None + assert cfgParser.rdBool("main", "blabla", None) is None + + # Read Integer + assert cfgParser.rdInt("main", "intopt1", 13) == 42 + assert cfgParser.rdInt("main", "intopt2", 13) == 13 + assert cfgParser.rdInt("main", "stropt", 13) == 13 + + assert cfgParser.rdInt("nope", "intopt1", 13) == 13 + assert cfgParser.rdInt("main", "blabla", 13) == 13 + + # Read String List + assert cfgParser.rdStrList("main", "list1", []) == [] + assert cfgParser.rdStrList("main", "list1", ["x"]) == ["a"] + assert cfgParser.rdStrList("main", "list1", ["x", "y"]) == ["a", "b"] + assert cfgParser.rdStrList("main", "list1", ["x", "y", "z"]) == ["a", "b", "c"] + assert cfgParser.rdStrList("main", "list1", ["x", "y", "z", "w"]) == ["a", "b", "c", "w"] + + assert cfgParser.rdStrList("main", "stropt", ["x"]) == ["value"] + assert cfgParser.rdStrList("main", "intopt1", ["x"]) == ["42"] + + assert cfgParser.rdStrList("nope", "list1", ["x"]) == ["x"] + assert cfgParser.rdStrList("main", "blabla", ["x"]) == ["x"] + + # Read Integer List + assert cfgParser.rdIntList("main", "list2", []) == [] + assert cfgParser.rdIntList("main", "list2", [1]) == [17] + assert cfgParser.rdIntList("main", "list2", [1, 2]) == [17, 18] + assert cfgParser.rdIntList("main", "list2", [1, 2, 3]) == [17, 18, 19] + assert cfgParser.rdIntList("main", "list2", [1, 2, 3, 4]) == [17, 18, 19, 4] + + assert cfgParser.rdIntList("main", "stropt", [1]) == [1] + assert cfgParser.rdIntList("main", "boolopt1", [1]) == [1] + + assert cfgParser.rdIntList("nope", "list2", [1]) == [1] + assert cfgParser.rdIntList("main", "blabla", [1]) == [1] + + # Internal + # ======== + + assert cfgParser._parseLine("main", "stropt", None, 999) is None + +# END Test testBaseCommon_NWConfigParser diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py index 14e24ec3..6dff0402 100644 --- a/tests/test_base/test_base_config.py +++ b/tests/test_base/test_base_config.py @@ -19,10 +19,9 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -import pytest -import sys import os -import configparser +import sys +import pytest from shutil import copyfile @@ -161,6 +160,20 @@ def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): assert tstConf.hasError is False assert tstConf.errData == [] + # Check handling of novelWriter as a package + with monkeypatch.context() as mp: + tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir) + assert tstConf.confPath == tmpDir + assert tstConf.dataPath == tmpDir + appRoot = tstConf.appRoot + + mp.setattr("os.path.isfile", lambda *a: True) + tstConf.initConfig(confPath=tmpDir, dataPath=tmpDir) + assert tstConf.confPath == tmpDir + assert tstConf.dataPath == tmpDir + assert tstConf.appRoot == os.path.dirname(appRoot) + assert tstConf.appPath == os.path.dirname(appRoot) + assert tstConf.loadConfig() assert tstConf.saveConfig() @@ -187,19 +200,34 @@ def testBaseConfig_Init(monkeypatch, tmpDir, fncDir, outDir, refDir, filesDir): assert tstConf.saveConfig() # Localisation + # ============ + i18nDir = os.path.join(fncDir, "i18n") os.mkdir(i18nDir) os.mkdir(os.path.join(i18nDir, "stuff")) tstConf.nwLangPath = i18nDir - copyfile(os.path.join(filesDir, "nw_en_GB.qm"), os.path.join(fncDir, "nw_en_GB.qm")) + copyfile(os.path.join(filesDir, "nw_en_GB.qm"), os.path.join(i18nDir, "nw_en_GB.qm")) writeFile(os.path.join(i18nDir, "nw_en_GB.ts"), "") writeFile(os.path.join(i18nDir, "nw_abcd.qm"), "") tstApp = MockApp() tstConf.initLocalisation(tstApp) + + # Check Lists theList = tstConf.listLanguages(tstConf.LANG_NW) assert theList == [("en_GB", "British English")] + theList = tstConf.listLanguages(tstConf.LANG_PROJ) + assert theList == [("en", "English")] + theList = tstConf.listLanguages(None) + assert theList == [] + + # Add Language + copyfile(os.path.join(filesDir, "nw_en_GB.qm"), os.path.join(i18nDir, "nw_fr.qm")) + writeFile(os.path.join(i18nDir, "nw_fr.ts"), "") + + theList = tstConf.listLanguages(tstConf.LANG_NW) + assert theList == [("en_GB", "British English"), ("fr", "Français")] copyfile(confFile, testFile) assert cmpFiles(testFile, compFile, [2, 9, 10]) @@ -311,6 +339,7 @@ def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir): # GUI Scaling # =========== + tmpConf.guiScale = 1.0 assert tmpConf.pxInt(10) == 10 assert tmpConf.pxInt(13) == 13 @@ -343,6 +372,19 @@ def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir): assert tmpConf.setWinSize(1200, 650) + # Preferences Size + tmpConf.guiScale = 2.0 + assert tmpConf.setPreferencesSize(70, 70) + assert tmpConf.getPreferencesSize() == [70, 70] + assert tmpConf.prefGeometry == [35, 35] + + tmpConf.guiScale = 1.0 + assert tmpConf.setPreferencesSize(70, 70) + assert tmpConf.getPreferencesSize() == [70, 70] + assert tmpConf.prefGeometry == [70, 70] + + assert tmpConf.setPreferencesSize(700, 615) + # Project Tree Columns tmpConf.guiScale = 2.0 assert tmpConf.setTreeColWidths([10, 20, 25]) @@ -436,6 +478,7 @@ def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir): # Getters Only # ============ + tmpConf.guiScale = 1.0 assert tmpConf.getTextWidth() == 600 assert tmpConf.getTextMargin() == 40 @@ -450,6 +493,7 @@ def testBaseConfig_SettersGetters(tmpConf, tmpDir, outDir, refDir): # Flag Setters # ============ + assert not tmpConf.setShowRefPanel(False) assert not tmpConf.showRefPanel assert tmpConf.setShowRefPanel(True) @@ -482,74 +526,16 @@ def testBaseConfig_Internal(monkeypatch, tmpConf): # Function _packList assert tmpConf._packList(["A", 1, 2.0, None, False]) == "A, 1, 2.0, None, False" - # Function _unpackList - assert tmpConf._unpackList("1, 2, 3", [0, 0, 0], tmpConf.CNF_I_LST) == [1, 2, 3] - assert tmpConf._unpackList("1, 2 ", [0, 0, 0], tmpConf.CNF_I_LST) == [1, 2, 0] - assert tmpConf._unpackList("A, B, C", [0, 0, 0], tmpConf.CNF_I_LST) == [0, 0, 0] - assert tmpConf._unpackList("1, 2, 3", ["X", "Y", "Z"], tmpConf.CNF_S_LST) == ["1", "2", "3"] - assert tmpConf._unpackList("A, B ", ["X", "Y", "Z"], tmpConf.CNF_S_LST) == ["A", "B", "Z"] - assert tmpConf._unpackList("A, B, C", ["X", "Y", "Z"], tmpConf.CNF_S_LST) == ["A", "B", "C"] - assert tmpConf._unpackList("A, B, C", ["X", "Y", "Z"], tmpConf.CNF_STR) == ["X", "Y", "Z"] - - # Function _parseLine - cnfParse = configparser.ConfigParser() - cnfParse.read_string( - "[Main]\n" - "val_string = stuff\n" - "val_int = 123\n" - "val_bool = True\n" - "val_list_string = A, B, C\n" - "val_list_int = 1, 2, 3\n" - ) - - assert tmpConf._parseLine( - cnfParse, "Main", "val_string", tmpConf.CNF_STR, "default" - ) == "stuff" - assert tmpConf._parseLine( - cnfParse, "Main", "nope", tmpConf.CNF_STR, "default" - ) == "default" - - assert tmpConf._parseLine( - cnfParse, "Main", "val_int", tmpConf.CNF_INT, "0" - ) == 123 - assert tmpConf._parseLine( - cnfParse, "Main", "nope", tmpConf.CNF_INT, 0 - ) == 0 - assert tmpConf._parseLine( - cnfParse, "Main", "val_string", tmpConf.CNF_INT, 0 - ) == 0 - - assert tmpConf._parseLine( - cnfParse, "Main", "val_bool", tmpConf.CNF_BOOL, False - ) is True - assert tmpConf._parseLine( - cnfParse, "Main", "nope", tmpConf.CNF_BOOL, False - ) is False - assert tmpConf._parseLine( - cnfParse, "Main", "val_string", tmpConf.CNF_BOOL, False - ) is False - - assert tmpConf._parseLine( - cnfParse, "Main", "val_list_string", tmpConf.CNF_S_LST, ["W", "X", "Y", "Z"] - ) == ["A", "B", "C", "Z"] - assert tmpConf._parseLine( - cnfParse, "Main", "nope", tmpConf.CNF_S_LST, ["W", "X", "Y", "Z"] - ) == ["W", "X", "Y", "Z"] - - assert tmpConf._parseLine( - cnfParse, "Main", "val_list_int", tmpConf.CNF_I_LST, [6, 7, 8, 9] - ) == [1, 2, 3, 9] - assert tmpConf._parseLine( - cnfParse, "Main", "nope", tmpConf.CNF_S_LST, [6, 7, 8, 9] - ) == [6, 7, 8, 9] - # Function _checkNone assert tmpConf._checkNone(None) is None assert tmpConf._checkNone("None") is None - assert tmpConf._checkNone("stuff") == "stuff" + assert tmpConf._checkNone("none") is None + assert tmpConf._checkNone("NONE") is None + assert tmpConf._checkNone("NoNe") is None + assert tmpConf._checkNone(123456) == 123456 # Function _checkOptionalPackages - # (Assumes enchant package exists ans is importable) + # (Assumes enchant package exists and is importable) tmpConf._checkOptionalPackages() assert tmpConf.hasEnchant is True @@ -559,12 +545,12 @@ def testBaseConfig_Internal(monkeypatch, tmpConf): assert tmpConf.hasEnchant is False with monkeypatch.context() as mp: - mp.setattr("shutil.which", lambda *args: "stuff") + mp.setattr("shutil.which", lambda *a: "stuff") tmpConf._checkOptionalPackages() assert tmpConf.hasAssistant is True with monkeypatch.context() as mp: - mp.setattr("shutil.which", lambda *args: None) + mp.setattr("shutil.which", lambda *a: None) tmpConf._checkOptionalPackages() assert tmpConf.hasAssistant is False