Merge branch 'dev' into item_layouts

This commit is contained in:
Veronica K. B. Olsen
2021-01-29 20:20:16 +01:00
15 changed files with 879 additions and 132 deletions
+3 -3
View File
@@ -32,7 +32,7 @@ import logging
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QErrorMessage
from nw.error import exceptionHandler
from nw.error import exceptionHandler, logException
from nw.config import Config
##
@@ -268,9 +268,9 @@ def main(sysArgs=None):
bundle = NSBundle.mainBundle()
info = bundle.localizedInfoDictionary() or bundle.infoDictionary()
info["CFBundleName"] = "novelWriter"
except ImportError as e:
except ImportError:
logger.error("Failed to set application name")
logger.error(str(e))
logException()
# Import GUI (after dependency checks), and launch
from nw.guimain import GuiMain
+34 -4
View File
@@ -30,7 +30,9 @@ from datetime import datetime
from PyQt5.QtWidgets import qApp
from nw.constants import nwConst, nwUnicode
from nw.constants import (
nwConst, nwUnicode, nwItemClass, nwItemType, nwItemLayout
)
logger = logging.getLogger(__name__)
@@ -103,11 +105,39 @@ def isHandle(theString):
return False
if len(theString) != 13:
return False
invalidChar = False
for c in theString:
if c not in "0123456789abcdef":
invalidChar = True
return not invalidChar
return False
return True
def isTitleTag(theString):
"""Check if a string is a valid title string.
"""
if not isinstance(theString, str):
return False
if len(theString) != 7:
return False
if not theString.startswith("T"):
return False
for c in theString[1:]:
if c not in "0123456789":
return False
return True
def isItemClass(theString):
"""Check if an item is a calid nwItemClass identifier.
"""
return theString in nwItemClass.__members__
def isItemType(theString):
"""Check if an item is a calid nwItemType identifier.
"""
return theString in nwItemType.__members__
def isItemLayout(theString):
"""Check if an item is a calid nwItemLayout identifier.
"""
return theString in nwItemLayout.__members__
def hexToInt(value, default=0):
"""Convert a hex string to an integer.
+7 -6
View File
@@ -38,6 +38,7 @@ from PyQt5.QtCore import QT_VERSION_STR, QStandardPaths, QSysInfo
from nw.constants import nwConst, nwFiles, nwUnicode
from nw.common import splitVersionNumber, formatTimeStamp
from nw.error import logException
logger = logging.getLogger(__name__)
@@ -293,7 +294,7 @@ class Config:
os.mkdir(self.confPath)
except Exception as e:
logger.error("Could not create folder: %s" % self.confPath)
logger.error(str(e))
logException()
self.hasError = True
self.errData.append("Could not create folder: %s" % self.confPath)
self.errData.append(str(e))
@@ -316,7 +317,7 @@ class Config:
os.mkdir(self.dataPath)
except Exception as e:
logger.error("Could not create folder: %s" % self.dataPath)
logger.error(str(e))
logException()
self.hasError = True
self.errData.append("Could not create folder: %s" % self.dataPath)
self.errData.append(str(e))
@@ -361,7 +362,7 @@ class Config:
cnfParse.read_file(inFile)
except Exception as e:
logger.error("Could not load config file")
logger.error(str(e))
logException()
self.hasError = True
self.errData.append("Could not load config file")
self.errData.append(str(e))
@@ -702,7 +703,7 @@ class Config:
self.confChanged = False
except Exception as e:
logger.error("Could not save config file")
logger.error(str(e))
logException()
self.hasError = True
self.errData.append("Could not save config file")
self.errData.append(str(e))
@@ -978,9 +979,9 @@ class Config:
return self._unpackList(
cnfParse.get(cnfSec, cnfName), cnfDefault, self.CNF_S_LST
)
except ValueError as e:
except ValueError:
logger.error("Failed to load value from config file.")
logger.error(str(e))
logException()
return cnfDefault
return cnfDefault
+162 -30
View File
@@ -36,11 +36,13 @@ from nw.constants import (
)
from nw.core.document import NWDoc
from nw.core.tools import countWords
from nw.common import isHandle, isTitleTag, isItemClass, isItemLayout
logger = logging.getLogger(__name__)
class NWIndex():
H_VALID = ("H0", "H1", "H2", "H3", "H4")
H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
def __init__(self, theProject, theParent):
@@ -155,9 +157,14 @@ class NWIndex():
try:
with open(indexFile, mode="r", encoding="utf8") as inFile:
theData = json.load(inFile)
except Exception as e:
except Exception:
logger.error("Failed to load index file")
logger.error(str(e))
nw.logException()
self.indexBroken = True
self.theParent.makeAlert(
"Could not load cached index file. Rebuilding index.",
nwAlert.WARN
)
return False
self._tagIndex = theData.get("tagIndex", {})
@@ -193,9 +200,9 @@ class NWIndex():
"textCounts" : self._textCounts,
"firstTitle" : self._firstTitle,
}, outFile, indent=2)
except Exception as e:
except Exception:
logger.error("Failed to save index file")
logger.error(str(e))
nw.logException()
return False
return True
@@ -205,43 +212,29 @@ class NWIndex():
elements it should.
"""
logger.debug("Checking index")
self.indexBroken = False
tStart = time()
try:
for tTag in self._tagIndex:
if len(self._tagIndex[tTag]) != 4:
self.indexBroken = True
for tHandle in self._refIndex:
for sTitle in self._refIndex[tHandle]:
for tEntry in self._refIndex[tHandle][sTitle]["tags"]:
if len(tEntry) != 3:
self.indexBroken = True
for tHandle in self._novelIndex:
for sLine in self._novelIndex[tHandle]:
if len(self._novelIndex[tHandle][sLine].keys()) != 8:
self.indexBroken = True
for tHandle in self._noteIndex:
for sLine in self._noteIndex[tHandle]:
if len(self._noteIndex[tHandle][sLine].keys()) != 8:
self.indexBroken = True
for tHandle in self._textCounts:
if len(self._textCounts[tHandle]) != 3:
self.indexBroken = True
self._checkTagIndex()
self._checkRefIndex()
self._checkNovelNoteIndex("novelIndex")
self._checkNovelNoteIndex("noteIndex")
self._checkTextCounts()
self.indexBroken = False
for tHandle in self._firstTitle:
if len(self._firstTitle[tHandle]) != 2:
self.indexBroken = True
except Exception as e:
except Exception:
logger.error("Error while checking index")
logger.error(str(e))
nw.logException()
self.indexBroken = True
tEnd = time()
logger.debug("Index check took %.3f ms" % ((tEnd - tStart)*1000))
logger.debug("Index check complete")
if self.indexBroken:
self.clearIndex()
self.theParent.makeAlert(
@@ -765,4 +758,143 @@ class NWIndex():
return theHandles
##
# Index Checkers
##
def _checkTagIndex(self):
"""Scan the tag index for errors.
Waring: This function raises exceptions.
"""
for tTag in self._tagIndex:
if not isinstance(tTag, str):
raise KeyError("tagIndex key is not a string")
tEntry = self._tagIndex[tTag]
if len(tEntry) != 4:
raise IndexError("tagIndex[a] expected 4 values")
if not isinstance(tEntry[0], int):
raise ValueError("tagIndex[a][0] is not an integer")
if not isHandle(tEntry[1]):
raise ValueError("tagIndex[a][1] is not a handle")
if not isItemClass(tEntry[2]):
raise ValueError("tagIndex[a][2] is not an nwItemClass")
if not isTitleTag(tEntry[3]):
raise ValueError("tagIndex[a][3] is not a title tag")
return
def _checkRefIndex(self):
"""Scan the reference index for errors.
Waring: This function raises exceptions.
"""
for tHandle in self._refIndex:
if not isHandle(tHandle):
raise KeyError("refIndex key is not a handle")
hEntry = self._refIndex[tHandle]
for sTitle in hEntry:
if not isTitleTag(sTitle):
raise KeyError("refIndex[a] key is not a title tag")
sEntry = hEntry[sTitle]
if "tags" not in sEntry:
raise KeyError("refIndex[a][b] has no 'tag' key")
for tEntry in sEntry["tags"]:
if len(tEntry) != 3:
raise IndexError("refIndex[a][b][tags][i] expected 3 values")
if not isinstance(tEntry[0], int):
raise ValueError("refIndex[a][b][tags][i][0] is not an integer")
if not tEntry[1] in nwKeyWords.VALID_KEYS:
raise ValueError("refIndex[a][b][tags][i][1] is not a keyword")
if not isinstance(tEntry[2], str):
raise ValueError("refIndex[a][b][tags][i][2] is not a string")
if "updated" not in sEntry:
raise KeyError("refIndex[a][b] has no 'updated' key")
if not isinstance(sEntry["updated"], int):
raise ValueError("%refIndex[a][b][updated] is not an integer")
return
def _checkNovelNoteIndex(self, idxName):
"""Scan the novel or note index for errors.
Waring: This function raises exceptions.
"""
if idxName == "novelIndex":
theIndex = self._novelIndex
elif idxName == "noteIndex":
theIndex = self._noteIndex
else:
raise IndexError("Unknown index %s" % idxName)
for tHandle in theIndex:
if not isHandle(tHandle):
raise KeyError("%s key is not a handle" % idxName)
hEntry = theIndex[tHandle]
for sTitle in theIndex[tHandle]:
if not isTitleTag(sTitle):
raise KeyError("%s[a] key is not a title tag" % idxName)
sEntry = hEntry[sTitle]
if len(sEntry) != 8:
raise IndexError("%s[a][b] expected 8 values" % idxName)
if "level" not in sEntry:
raise KeyError("%s[a][b] has no 'level' key" % idxName)
if "title" not in sEntry:
raise KeyError("%s[a][b] has no 'title' key" % idxName)
if "layout" not in sEntry:
raise KeyError("%s[a][b] has no 'layout' key" % idxName)
if "synopsis" not in sEntry:
raise KeyError("%s[a][b] has no 'synopsis' key" % idxName)
if "cCount" not in sEntry:
raise KeyError("%s[a][b] has no 'cCount' key" % idxName)
if "wCount" not in sEntry:
raise KeyError("%s[a][b] has no 'wCount' key" % idxName)
if "pCount" not in sEntry:
raise KeyError("%s[a][b] has no 'pCount' key" % idxName)
if "updated" not in sEntry:
raise KeyError("%s[a][b] has no 'updated' key" % idxName)
if not sEntry["level"] in self.H_VALID:
raise ValueError("%s[a][b][level] is not a header level" % idxName)
if not isinstance(sEntry["title"], str):
raise ValueError("%s[a][b][title] is not a string" % idxName)
if not isItemLayout(sEntry["layout"]):
raise ValueError("%s[a][b][layout] is not an nwItemLayout" % idxName)
if not isinstance(sEntry["synopsis"], str):
raise ValueError("%s[a][b][synopsis] is not a string" % idxName)
if not isinstance(sEntry["cCount"], int):
raise ValueError("%s[a][b][cCount] is not an integer" % idxName)
if not isinstance(sEntry["wCount"], int):
raise ValueError("%s[a][b][wCount] is not an integer" % idxName)
if not isinstance(sEntry["pCount"], int):
raise ValueError("%s[a][b][pCount] is not an integer" % idxName)
if not isinstance(sEntry["updated"], int):
raise ValueError("%s[a][b][updated] is not an integer" % idxName)
return
def _checkTextCounts(self):
"""Scan the text counts index for errors.
Waring: This function raises exceptions.
"""
for tHandle in self._textCounts:
if not isHandle(tHandle):
raise KeyError("textCounts key is not a handle")
tEntry = self._textCounts[tHandle]
if len(tEntry) != 3:
raise IndexError("textCounts[a] expected 3 values")
if not isinstance(tEntry[0], int):
raise ValueError("textCounts[a][0] is not an integer")
if not isinstance(tEntry[1], int):
raise ValueError("textCounts[a][1] is not an integer")
if not isinstance(tEntry[2], int):
raise ValueError("textCounts[a][2] is not an integer")
return
# END Class NWIndex
+6 -4
View File
@@ -28,8 +28,10 @@ import logging
from lxml import etree
from nw.common import checkInt, isHandle
from nw.constants import nwItemType, nwItemClass, nwItemLayout
from nw.common import (
checkInt, isHandle, isItemClass, isItemLayout, isItemType
)
logger = logging.getLogger(__name__)
@@ -201,7 +203,7 @@ class NWItem():
"""
if isinstance(theType, nwItemType):
self.itemType = theType
elif theType in nwItemType.__members__:
elif isItemType(theType):
self.itemType = nwItemType[theType]
else:
logger.error("Unrecognised item type '%s'" % theType)
@@ -214,7 +216,7 @@ class NWItem():
"""
if isinstance(theClass, nwItemClass):
self.itemClass = theClass
elif theClass in nwItemClass.__members__:
elif isItemClass(theClass):
self.itemClass = nwItemClass[theClass]
else:
logger.error("Unrecognised item class '%s'" % theClass)
@@ -227,7 +229,7 @@ class NWItem():
"""
if isinstance(theLayout, nwItemLayout):
self.itemLayout = theLayout
elif theLayout in nwItemLayout.__members__:
elif isItemLayout(theLayout):
self.itemLayout = nwItemLayout[theLayout]
else:
logger.error("Unrecognised item layout '%s'" % theLayout)
+5 -4
View File
@@ -25,6 +25,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import nw
import logging
import json
import os
@@ -121,9 +122,9 @@ class OptionState():
try:
with open(stateFile, mode="r", encoding="utf8") as inFile:
theState = json.load(inFile)
except Exception as e:
except Exception:
logger.error("Failed to load GUI options file")
logger.error(str(e))
nw.logException()
return False
# Filter out unused variables
@@ -148,9 +149,9 @@ class OptionState():
try:
with open(stateFile, mode="w+", encoding="utf8") as outFile:
json.dump(self.theState, outFile, indent=2)
except Exception as e:
except Exception:
logger.error("Failed to save GUI options file")
logger.error(str(e))
nw.logException()
return False
return True
+23 -18
View File
@@ -1219,9 +1219,9 @@ class NWProject():
if len(theLines) != 4:
return ["ERROR"]
except Exception as e:
except Exception:
logger.error("Failed to read project lockfile")
logger.error(str(e))
nw.logException()
return ["ERROR"]
return theLines
@@ -1240,9 +1240,9 @@ class NWProject():
outFile.write("%s\n" % self.mainConf.kernelVer)
outFile.write("%d\n" % time())
except Exception as e:
except Exception:
logger.error("Failed to write project lockfile")
logger.error(str(e))
nw.logException()
return False
return True
@@ -1257,9 +1257,9 @@ class NWProject():
if os.path.isfile(lockFile):
try:
os.unlink(lockFile)
except Exception as e:
except Exception:
logger.error("Failed to remove project lockfile")
logger.error(str(e))
nw.logException()
return False
return True
@@ -1415,9 +1415,9 @@ class NWProject():
self.notesWCount,
))
except Exception as e:
except Exception:
logger.error("Failed to write session stats file")
logger.error(str(e))
nw.logException()
return False
return True
@@ -1453,17 +1453,19 @@ class NWProject():
os.rename(theFile, newPath)
logger.info("Moved file: %s" % theFile)
logger.info("New location: %s" % newPath)
except Exception as e:
logger.error(str(e))
except Exception:
errList.append("Could not move: %s" % theFile)
logger.error("Could not move: %s" % theFile)
nw.logException()
elif len(dataItem) == 21 and dataItem.endswith("_main.bak"):
try:
os.unlink(theFile)
logger.info("Deleted file: %s" % theFile)
except Exception as e:
logger.error(str(e))
except Exception:
errList.append("Could not delete: %s" % theFile)
logger.error("Could not delete: %s" % theFile)
nw.logException()
else:
theErr = self._moveUnknownItem(theData, dataItem)
@@ -1475,9 +1477,10 @@ class NWProject():
try:
os.rmdir(theData)
logger.info("Removed folder: %s" % theFolder)
except Exception as e:
logger.error(str(e))
except Exception:
errList.append("Failed to remove: %s" % theFolder)
logger.error("Failed to remove: %s" % theFolder)
nw.logException()
return errList
@@ -1495,8 +1498,9 @@ class NWProject():
try:
os.rename(theSrc, theDst)
logger.info("Moved to junk: %s" % theSrc)
except Exception as e:
logger.error(str(e))
except Exception:
logger.error("Could not move item %s to junk." % theSrc)
nw.logException()
return "Could not move item %s to junk." % theSrc
return ""
@@ -1529,8 +1533,9 @@ class NWProject():
logger.info("Deleting: %s" % rmFile)
try:
os.unlink(rmFile)
except Exception as e:
logger.error(str(e))
except Exception:
logger.error("Could not delete: %s" % rmFile)
nw.logException()
return False
return True
+8 -8
View File
@@ -72,9 +72,9 @@ class NWSpellCheck():
with open(self.projectDict, mode="a+", encoding="utf-8") as outFile:
outFile.write("%s\n" % newWord)
self.projDict.append(newWord)
except Exception as e:
except Exception:
logger.error("Failed to add word to project word list %s" % str(self.projectDict))
logger.error(str(e))
nw.logException()
return False
return True
return False
@@ -123,9 +123,9 @@ class NWSpellCheck():
if len(theLine) > 0 and theLine not in self.projDict:
self.projDict.append(theLine)
logger.debug("Project word list contains %d words" % len(self.projDict))
except Exception as e:
except Exception:
logger.error("Failed to load project word list")
logger.error(str(e))
nw.logException()
return False
return True
@@ -201,9 +201,9 @@ class NWSpellEnchant(NWSpellCheck):
try:
spTag = self.theDict.tag
spName = self.theDict.provider.name
except Exception as e:
except Exception:
logger.error("Failed to extract information about the dictionary")
logger.error(str(e))
nw.logException()
spTag = ""
spName = ""
@@ -261,9 +261,9 @@ class NWSpellSimple(NWSpellCheck):
logger.debug("Spell check word list for language %s loaded" % theLang)
logger.debug("Word list contains %d words" % len(self.WORDS))
self.spellLanguage = theLang
except Exception as e:
except Exception:
logger.error("Failed to load spell check word list for language %s" % theLang)
logger.error(str(e))
nw.logException()
self.spellLanguage = None
self._readProjectDictionary(projectDict)
+4 -2
View File
@@ -24,6 +24,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import nw
import logging
import os
@@ -204,8 +205,9 @@ class NWTree():
outFile.write("\n".join(tocList))
outFile.write("\n")
except Exception as e:
logger.error(str(e))
except Exception:
logger.error("Could not write ToC file")
nw.logException()
return False
return True
+19 -4
View File
@@ -24,12 +24,31 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import sys
import logging
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
qApp, QDialog, QGridLayout, QStyle, QPlainTextEdit, QLabel,
QDialogButtonBox
)
logger = logging.getLogger(__name__)
# =============================================================================================== #
# Utility Functions
# =============================================================================================== #
def logException():
"""Log the content of an exception message.
"""
exType, exValue, _ = sys.exc_info()
logger.error("%s: %s" % (exType.__name__, str(exValue).strip("'")))
# =============================================================================================== #
# Error Handler
# =============================================================================================== #
class NWErrorMessage(QDialog):
def __init__(self, parent):
@@ -72,7 +91,6 @@ class NWErrorMessage(QDialog):
"""Generate a message and append session data, error info and
error traceback.
"""
import sys
from traceback import format_tb
from nw import __issuesurl__, __version__
from PyQt5.Qt import PYQT_VERSION_STR
@@ -133,15 +151,12 @@ class NWErrorMessage(QDialog):
# END Class NWErrorMessage
def exceptionHandler(exType, exValue, exTrace):
"""Function to catch unhandled global exceptions.
"""
import logging
from traceback import print_tb
from PyQt5.QtWidgets import qApp
logger = logging.getLogger(__name__)
logger.critical("%s: %s" % (exType.__name__, str(exValue)))
print_tb(exTrace)
+6 -6
View File
@@ -671,9 +671,9 @@ class GuiBuildNovel(QDialog):
bldObj.doConvert()
bldObj.doPostProcessing()
except Exception as e:
except Exception:
logger.error("Failed to generate html of document '%s'" % tItem.itemHandle)
logger.error(str(e))
nw.logException()
if isPreview:
self.docView.setText((
"Failed to generate preview. "
@@ -997,9 +997,9 @@ class GuiBuildNovel(QDialog):
with open(buildCache, mode="r", encoding="utf8") as inFile:
theJson = inFile.read()
theData = json.loads(theJson)
except Exception as e:
except Exception:
logger.error("Failed to load build cache")
logger.error(str(e))
nw.logException()
return False
if "htmlText" in theData.keys():
@@ -1026,9 +1026,9 @@ class GuiBuildNovel(QDialog):
"htmlStyle" : self.htmlStyle,
"buildTime" : self.buildTime,
}, indent=2))
except Exception as e:
except Exception:
logger.error("Failed to save build cache")
logger.error(str(e))
nw.logException()
return False
return True
+2 -2
View File
@@ -180,9 +180,9 @@ class GuiDocViewer(QTextBrowser):
aDoc.tokenizeText()
aDoc.doConvert()
aDoc.doPostProcessing()
except Exception as e:
except Exception:
logger.error("Failed to generate preview for document with handle '%s'" % tHandle)
logger.error(str(e))
nw.logException()
self.setText("An error occurred while generating the preview.")
return False
+8 -8
View File
@@ -265,9 +265,9 @@ class GuiTheme:
if os.path.isfile(self.cssFile):
with open(self.cssFile, mode="r", encoding="utf8") as inFile:
cssData = inFile.read()
except Exception as e:
except Exception:
logger.error("Could not load theme css file")
logger.error(str(e))
nw.logException()
return False
# Config File
@@ -275,9 +275,9 @@ class GuiTheme:
try:
with open(self.confFile, mode="r", encoding="utf8") as inFile:
confParser.read_file(inFile)
except Exception as e:
except Exception:
logger.error("Could not load theme settings from: %s" % self.confFile)
logger.error(str(e))
nw.logException()
return False
## Main
@@ -333,9 +333,9 @@ class GuiTheme:
try:
with open(self.syntaxFile, mode="r", encoding="utf8") as inFile:
confParser.read_file(inFile)
except Exception as e:
except Exception:
logger.error("Could not load syntax colours from: %s" % self.syntaxFile)
logger.error(str(e))
nw.logException()
return False
## Main
@@ -637,9 +637,9 @@ class GuiIcons:
try:
with open(self.confFile, mode="r", encoding="utf8") as inFile:
confParser.read_file(inFile)
except Exception as e:
except Exception:
logger.error("Could not load icon theme settings from: %s" % self.confFile)
logger.error(str(e))
nw.logException()
return False
## Main